@hectasquare/medusa-plugin-category-tree 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/components/category-tree/canvas-dock.d.ts +5 -0
- package/.medusa/server/src/admin/components/category-tree/canvas-dock.js +4 -2
- package/.medusa/server/src/admin/components/category-tree/category-node.js +12 -2
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +42 -3
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +5 -1
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.d.ts +9 -1
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.js +116 -30
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.d.ts +14 -0
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.js +95 -0
- package/.medusa/server/src/admin/components/category-tree/tier-bars.d.ts +4 -2
- package/.medusa/server/src/admin/components/category-tree/tier-bars.js +9 -4
- package/.medusa/server/src/admin/components/category-tree/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/admin/components/category-tree/canvas-dock.tsx +71 -18
- package/src/admin/components/category-tree/category-node.tsx +48 -9
- package/src/admin/components/category-tree/category-tree-canvas.tsx +56 -2
- package/src/admin/components/category-tree/edit-category-drawer.tsx +54 -0
- package/src/admin/components/category-tree/lib/layout-engine.ts +152 -25
- package/src/admin/components/category-tree/lib/tree-state.ts +112 -0
- package/src/admin/components/category-tree/tier-bars.tsx +12 -5
- package/src/admin/components/category-tree/types.ts +4 -0
|
@@ -19,6 +19,7 @@ export type CategoryTreeNode = {
|
|
|
19
19
|
sibling_rank: number;
|
|
20
20
|
children_count: number;
|
|
21
21
|
has_children: boolean;
|
|
22
|
+
branch_color?: string;
|
|
22
23
|
};
|
|
23
24
|
export type CategoryTreeResponse = {
|
|
24
25
|
categories: CategoryTreeNode[];
|
|
@@ -35,6 +36,7 @@ export type CategoryNodeData = {
|
|
|
35
36
|
tier: number;
|
|
36
37
|
siblingRank: number;
|
|
37
38
|
superscriptRank: string;
|
|
39
|
+
branchColor?: string;
|
|
38
40
|
salesChannels: SalesChannelInfo[];
|
|
39
41
|
isCollapsed: boolean;
|
|
40
42
|
hiddenDescendantCount: number;
|
|
@@ -44,4 +46,6 @@ export type CategoryNodeData = {
|
|
|
44
46
|
onAddChild: (parent: CategoryTreeNode) => void;
|
|
45
47
|
onEdit: (category: CategoryTreeNode) => void;
|
|
46
48
|
onDetach: (id: string) => void;
|
|
49
|
+
onMoveSiblingUp?: (id: string) => void;
|
|
50
|
+
onMoveSiblingDown?: (id: string) => void;
|
|
47
51
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hectasquare/medusa-plugin-category-tree",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AAA Interactive Category Tree & Mind Map for Medusa v2 Admin with drag-and-drop reparenting, tier indicators, superscript ranks, multi-storefront filtering, and NotebookLM-style subtree collapse.",
|
|
5
5
|
"author": "HectaSquare Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,13 @@ export interface CanvasDockProps {
|
|
|
22
22
|
onCollapseAll: () => void
|
|
23
23
|
onFocusRoot: () => void
|
|
24
24
|
|
|
25
|
+
selectedCount?: number
|
|
26
|
+
onExpandSelected?: () => void
|
|
27
|
+
onCollapseSelected?: () => void
|
|
28
|
+
|
|
29
|
+
isBoxSelect?: boolean
|
|
30
|
+
onToggleBoxSelect?: () => void
|
|
31
|
+
|
|
25
32
|
onZoomIn: () => void
|
|
26
33
|
onZoomOut: () => void
|
|
27
34
|
onFitView: () => void
|
|
@@ -45,6 +52,11 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
45
52
|
onExpandAll,
|
|
46
53
|
onCollapseAll,
|
|
47
54
|
onFocusRoot,
|
|
55
|
+
selectedCount = 0,
|
|
56
|
+
onExpandSelected,
|
|
57
|
+
onCollapseSelected,
|
|
58
|
+
isBoxSelect = false,
|
|
59
|
+
onToggleBoxSelect,
|
|
48
60
|
onFitView,
|
|
49
61
|
onOpenCreateRoot,
|
|
50
62
|
dirtyCount,
|
|
@@ -84,7 +96,7 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
84
96
|
</button>
|
|
85
97
|
</div>
|
|
86
98
|
|
|
87
|
-
<div className="relative w-
|
|
99
|
+
<div className="relative w-36 sm:w-44">
|
|
88
100
|
<input
|
|
89
101
|
type="text"
|
|
90
102
|
placeholder="Search categories…"
|
|
@@ -95,24 +107,47 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
95
107
|
<MagnifyingGlass className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-ui-fg-muted pointer-events-none" />
|
|
96
108
|
</div>
|
|
97
109
|
|
|
110
|
+
{/* Expand / Collapse Section with Selective awareness */}
|
|
98
111
|
<div className="flex items-center gap-1 border-l border-ui-border-base pl-2.5">
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
{selectedCount > 0 ? (
|
|
113
|
+
<>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
title={`Expand selected category (${selectedCount} selected)`}
|
|
117
|
+
onClick={onExpandSelected || onExpandAll}
|
|
118
|
+
className="px-2 py-1 text-xs font-medium bg-ui-bg-interactive text-ui-fg-on-color rounded-md transition-colors"
|
|
119
|
+
>
|
|
120
|
+
Expand ({selectedCount})
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
title={`Collapse selected category (${selectedCount} selected)`}
|
|
125
|
+
onClick={onCollapseSelected || onCollapseAll}
|
|
126
|
+
className="px-2 py-1 text-xs font-medium bg-ui-bg-subtle text-ui-fg-base hover:bg-ui-bg-base border border-ui-border-base rounded-md transition-colors"
|
|
127
|
+
>
|
|
128
|
+
Collapse ({selectedCount})
|
|
129
|
+
</button>
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
title="Expand All Categories"
|
|
136
|
+
onClick={onExpandAll}
|
|
137
|
+
className="px-2 py-1 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-md transition-colors"
|
|
138
|
+
>
|
|
139
|
+
Expand All
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
title="Collapse All Subcategories"
|
|
144
|
+
onClick={onCollapseAll}
|
|
145
|
+
className="px-2 py-1 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-md transition-colors"
|
|
146
|
+
>
|
|
147
|
+
Collapse All
|
|
148
|
+
</button>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
116
151
|
|
|
117
152
|
<button
|
|
118
153
|
type="button"
|
|
@@ -124,6 +159,24 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
124
159
|
</button>
|
|
125
160
|
</div>
|
|
126
161
|
|
|
162
|
+
{/* Box Selection / Marquee mode toggle */}
|
|
163
|
+
{onToggleBoxSelect && (
|
|
164
|
+
<div className="flex items-center border-l border-ui-border-base pl-2.5">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
title={isBoxSelect ? "Box Selection Active (Drag to outline cards). Click to switch to Pan" : "Click to enable Box Select marquee (Outline cards to move together)"}
|
|
168
|
+
onClick={onToggleBoxSelect}
|
|
169
|
+
className={`px-2 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
170
|
+
isBoxSelect
|
|
171
|
+
? "bg-blue-600 text-white shadow-xs"
|
|
172
|
+
: "text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle"
|
|
173
|
+
}`}
|
|
174
|
+
>
|
|
175
|
+
⛶ Box Select
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
127
180
|
<div className="flex items-center gap-1 border-l border-ui-border-base pl-2.5">
|
|
128
181
|
<button
|
|
129
182
|
type="button"
|
|
@@ -15,7 +15,9 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
15
15
|
const {
|
|
16
16
|
category,
|
|
17
17
|
tier,
|
|
18
|
+
siblingRank,
|
|
18
19
|
superscriptRank,
|
|
20
|
+
branchColor,
|
|
19
21
|
salesChannels,
|
|
20
22
|
isCollapsed,
|
|
21
23
|
hiddenDescendantCount,
|
|
@@ -25,6 +27,8 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
25
27
|
onAddChild,
|
|
26
28
|
onEdit,
|
|
27
29
|
onDetach,
|
|
30
|
+
onMoveSiblingUp,
|
|
31
|
+
onMoveSiblingDown,
|
|
28
32
|
} = data
|
|
29
33
|
|
|
30
34
|
const isTB = direction === "TB"
|
|
@@ -42,6 +46,13 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
42
46
|
? "ring-2 ring-ui-border-interactive border-transparent shadow-lg"
|
|
43
47
|
: "border-ui-border-base shadow-sm hover:shadow-md hover:border-ui-border-strong"
|
|
44
48
|
} ${isDimmed ? "opacity-35 grayscale" : "opacity-100"}`}
|
|
49
|
+
style={
|
|
50
|
+
selected && branchColor
|
|
51
|
+
? {
|
|
52
|
+
boxShadow: `0 0 0 2px ${branchColor}, 0 10px 15px -3px rgba(0, 0, 0, 0.1)`,
|
|
53
|
+
}
|
|
54
|
+
: undefined
|
|
55
|
+
}
|
|
45
56
|
>
|
|
46
57
|
<Handle
|
|
47
58
|
type="target"
|
|
@@ -49,7 +60,7 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
49
60
|
className="!w-2.5 !h-2.5 !bg-ui-border-interactive !border-2 !border-white dark:!border-neutral-900 transition-transform group-hover:scale-125"
|
|
50
61
|
/>
|
|
51
62
|
|
|
52
|
-
<TierBars tier={tier} />
|
|
63
|
+
<TierBars tier={tier} color={branchColor} />
|
|
53
64
|
|
|
54
65
|
<div className="flex flex-1 items-center gap-2.5 px-2.5 py-2 min-w-0 overflow-hidden">
|
|
55
66
|
<div className="relative w-11 h-11 rounded-lg overflow-hidden flex-shrink-0 border border-ui-border-base bg-ui-bg-subtle flex items-center justify-center shadow-2xs">
|
|
@@ -74,20 +85,48 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
74
85
|
</div>
|
|
75
86
|
|
|
76
87
|
<div className="flex flex-col min-w-0 flex-1 justify-center">
|
|
77
|
-
<div className="flex items-center gap-1
|
|
88
|
+
<div className="flex items-center gap-1 min-w-0">
|
|
78
89
|
<span
|
|
79
|
-
className="text-xs font-semibold text-ui-fg-base truncate leading-snug"
|
|
90
|
+
className="text-xs font-semibold text-ui-fg-base truncate leading-snug flex-1 min-w-0"
|
|
80
91
|
title={category.name}
|
|
81
92
|
>
|
|
82
93
|
{category.name}
|
|
83
94
|
</span>
|
|
84
95
|
{superscriptRank && (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
97
|
+
<span
|
|
98
|
+
className="text-[11px] font-bold font-mono text-ui-fg-subtle px-1 py-0.5 rounded bg-ui-bg-subtle/80"
|
|
99
|
+
title={`Rank #${siblingRank} among siblings (Storefront sort order)`}
|
|
100
|
+
>
|
|
101
|
+
{superscriptRank}
|
|
102
|
+
</span>
|
|
103
|
+
{onMoveSiblingUp && (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
title="Move Up in Storefront Order (↑)"
|
|
107
|
+
onClick={(e) => {
|
|
108
|
+
e.stopPropagation()
|
|
109
|
+
onMoveSiblingUp(id)
|
|
110
|
+
}}
|
|
111
|
+
className="w-3.5 h-3.5 flex items-center justify-center text-[10px] font-bold text-ui-fg-muted hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded transition-colors"
|
|
112
|
+
>
|
|
113
|
+
↑
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
{onMoveSiblingDown && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
title="Move Down in Storefront Order (↓)"
|
|
120
|
+
onClick={(e) => {
|
|
121
|
+
e.stopPropagation()
|
|
122
|
+
onMoveSiblingDown(id)
|
|
123
|
+
}}
|
|
124
|
+
className="w-3.5 h-3.5 flex items-center justify-center text-[10px] font-bold text-ui-fg-muted hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded transition-colors"
|
|
125
|
+
>
|
|
126
|
+
↓
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
91
130
|
)}
|
|
92
131
|
</div>
|
|
93
132
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Node,
|
|
12
12
|
useReactFlow,
|
|
13
13
|
ReactFlowProvider,
|
|
14
|
+
SelectionMode,
|
|
14
15
|
} from "@xyflow/react"
|
|
15
16
|
import "@xyflow/react/dist/style.css"
|
|
16
17
|
import { toast, TooltipProvider } from "@medusajs/ui"
|
|
@@ -24,11 +25,13 @@ import { CategoryTreeNode, SalesChannelInfo } from "./types"
|
|
|
24
25
|
import {
|
|
25
26
|
computeTiers,
|
|
26
27
|
computeSiblingRanks,
|
|
28
|
+
computeBranchColors,
|
|
29
|
+
moveSiblingRank,
|
|
27
30
|
wouldCreateCycle,
|
|
28
31
|
getVisibleTree,
|
|
29
32
|
computeMoves,
|
|
30
33
|
} from "./lib/tree-state"
|
|
31
|
-
import {
|
|
34
|
+
import { computeTreeLayout } from "./lib/layout-engine"
|
|
32
35
|
import { sdk } from "../../lib/sdk"
|
|
33
36
|
|
|
34
37
|
const nodeTypes = {
|
|
@@ -52,6 +55,8 @@ const CanvasInner: React.FC = () => {
|
|
|
52
55
|
const [selectedStorefront, setSelectedStorefront] = useState<string>("all")
|
|
53
56
|
const [searchQuery, setSearchQuery] = useState("")
|
|
54
57
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set())
|
|
58
|
+
const [isBoxSelect, setIsBoxSelect] = useState(false)
|
|
59
|
+
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([])
|
|
55
60
|
|
|
56
61
|
const [createDrawerOpen, setCreateDrawerOpen] = useState(false)
|
|
57
62
|
const [createParentCategory, setCreateParentCategory] = useState<CategoryTreeNode | null>(null)
|
|
@@ -95,6 +100,7 @@ const CanvasInner: React.FC = () => {
|
|
|
95
100
|
|
|
96
101
|
const tiers = computeTiers(categories)
|
|
97
102
|
const siblingRanks = computeSiblingRanks(categories)
|
|
103
|
+
const branchColors = computeBranchColors(categories)
|
|
98
104
|
|
|
99
105
|
const enriched: CategoryTreeNode[] = categories.map((cat) => {
|
|
100
106
|
const kids = map.get(cat.id) || []
|
|
@@ -105,6 +111,7 @@ const CanvasInner: React.FC = () => {
|
|
|
105
111
|
sibling_rank: rankInfo?.rankNum || 1,
|
|
106
112
|
children_count: kids.length,
|
|
107
113
|
has_children: kids.length > 0,
|
|
114
|
+
branch_color: branchColors.get(cat.id),
|
|
108
115
|
}
|
|
109
116
|
})
|
|
110
117
|
|
|
@@ -123,6 +130,32 @@ const CanvasInner: React.FC = () => {
|
|
|
123
130
|
})
|
|
124
131
|
}, [])
|
|
125
132
|
|
|
133
|
+
const handleExpandSelected = useCallback(() => {
|
|
134
|
+
if (selectedNodeIds.length === 0) return
|
|
135
|
+
setCollapsedIds((prev) => {
|
|
136
|
+
const next = new Set(prev)
|
|
137
|
+
selectedNodeIds.forEach((id) => next.delete(id))
|
|
138
|
+
return next
|
|
139
|
+
})
|
|
140
|
+
}, [selectedNodeIds])
|
|
141
|
+
|
|
142
|
+
const handleCollapseSelected = useCallback(() => {
|
|
143
|
+
if (selectedNodeIds.length === 0) return
|
|
144
|
+
setCollapsedIds((prev) => {
|
|
145
|
+
const next = new Set(prev)
|
|
146
|
+
selectedNodeIds.forEach((id) => next.add(id))
|
|
147
|
+
return next
|
|
148
|
+
})
|
|
149
|
+
}, [selectedNodeIds])
|
|
150
|
+
|
|
151
|
+
const handleMoveSiblingUp = useCallback((id: string) => {
|
|
152
|
+
setCategories((prev) => moveSiblingRank(prev, id, "up"))
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
const handleMoveSiblingDown = useCallback((id: string) => {
|
|
156
|
+
setCategories((prev) => moveSiblingRank(prev, id, "down"))
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
126
159
|
const handleAddChild = useCallback((parent: CategoryTreeNode) => {
|
|
127
160
|
setCreateParentCategory(parent)
|
|
128
161
|
setCreateDrawerOpen(true)
|
|
@@ -171,6 +204,7 @@ const CanvasInner: React.FC = () => {
|
|
|
171
204
|
tier: cat.tier,
|
|
172
205
|
siblingRank: rankInfo?.rankNum || 1,
|
|
173
206
|
superscriptRank: rankInfo?.superscript || "¹",
|
|
207
|
+
branchColor: cat.branch_color,
|
|
174
208
|
salesChannels,
|
|
175
209
|
isCollapsed: collapsedIds.has(cat.id),
|
|
176
210
|
hiddenDescendantCount: cat.children_count,
|
|
@@ -180,6 +214,8 @@ const CanvasInner: React.FC = () => {
|
|
|
180
214
|
onAddChild: handleAddChild,
|
|
181
215
|
onEdit: handleEdit,
|
|
182
216
|
onDetach: handleDetach,
|
|
217
|
+
onMoveSiblingUp: handleMoveSiblingUp,
|
|
218
|
+
onMoveSiblingDown: handleMoveSiblingDown,
|
|
183
219
|
},
|
|
184
220
|
}
|
|
185
221
|
})
|
|
@@ -192,6 +228,10 @@ const CanvasInner: React.FC = () => {
|
|
|
192
228
|
source: cat.parent_category_id,
|
|
193
229
|
target: cat.id,
|
|
194
230
|
type: "smoothstep",
|
|
231
|
+
style: {
|
|
232
|
+
stroke: cat.branch_color || "#64748b",
|
|
233
|
+
strokeWidth: 2,
|
|
234
|
+
},
|
|
195
235
|
})
|
|
196
236
|
}
|
|
197
237
|
}
|
|
@@ -203,7 +243,7 @@ const CanvasInner: React.FC = () => {
|
|
|
203
243
|
childrenMap
|
|
204
244
|
)
|
|
205
245
|
|
|
206
|
-
const { nodes: layoutedNodes, edges: layoutedEdges } =
|
|
246
|
+
const { nodes: layoutedNodes, edges: layoutedEdges } = computeTreeLayout(
|
|
207
247
|
visibleNodes,
|
|
208
248
|
visibleEdges,
|
|
209
249
|
{ direction }
|
|
@@ -223,6 +263,8 @@ const CanvasInner: React.FC = () => {
|
|
|
223
263
|
handleAddChild,
|
|
224
264
|
handleEdit,
|
|
225
265
|
handleDetach,
|
|
266
|
+
handleMoveSiblingUp,
|
|
267
|
+
handleMoveSiblingDown,
|
|
226
268
|
setNodes,
|
|
227
269
|
setEdges,
|
|
228
270
|
])
|
|
@@ -311,6 +353,11 @@ const CanvasInner: React.FC = () => {
|
|
|
311
353
|
onExpandAll={handleExpandAll}
|
|
312
354
|
onCollapseAll={handleCollapseAll}
|
|
313
355
|
onFocusRoot={handleFocusRoot}
|
|
356
|
+
selectedCount={selectedNodeIds.length}
|
|
357
|
+
onExpandSelected={handleExpandSelected}
|
|
358
|
+
onCollapseSelected={handleCollapseSelected}
|
|
359
|
+
isBoxSelect={isBoxSelect}
|
|
360
|
+
onToggleBoxSelect={() => setIsBoxSelect((prev) => !prev)}
|
|
314
361
|
onZoomIn={() => zoomIn({ duration: 300 })}
|
|
315
362
|
onZoomOut={() => zoomOut({ duration: 300 })}
|
|
316
363
|
onFitView={() => fitView({ padding: 0.25, duration: 400 })}
|
|
@@ -332,6 +379,13 @@ const CanvasInner: React.FC = () => {
|
|
|
332
379
|
onEdgesChange={onEdgesChange}
|
|
333
380
|
onConnect={onConnect}
|
|
334
381
|
onEdgesDelete={onEdgesDelete}
|
|
382
|
+
onSelectionChange={useCallback(({ nodes }: { nodes: Node[] }) => {
|
|
383
|
+
setSelectedNodeIds(nodes.map((n) => n.id))
|
|
384
|
+
}, [])}
|
|
385
|
+
selectionMode={SelectionMode.Partial}
|
|
386
|
+
selectionOnDrag={isBoxSelect}
|
|
387
|
+
panOnDrag={isBoxSelect ? [1, 2] : true}
|
|
388
|
+
elementsSelectable={true}
|
|
335
389
|
nodeTypes={nodeTypes}
|
|
336
390
|
edgeTypes={edgeTypes}
|
|
337
391
|
minZoom={0.2}
|
|
@@ -25,6 +25,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
25
25
|
const [handle, setHandle] = useState("")
|
|
26
26
|
const [description, setDescription] = useState("")
|
|
27
27
|
const [imageUrl, setImageUrl] = useState("")
|
|
28
|
+
const [rank, setRank] = useState(0)
|
|
28
29
|
const [isActive, setIsActive] = useState(true)
|
|
29
30
|
const [isInternal, setIsInternal] = useState(false)
|
|
30
31
|
const [selectedChannels, setSelectedChannels] = useState<string[]>([])
|
|
@@ -37,6 +38,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
37
38
|
setHandle(category.handle || "")
|
|
38
39
|
setDescription(category.description || "")
|
|
39
40
|
setImageUrl(category.image_url || "")
|
|
41
|
+
setRank(category.rank ?? 0)
|
|
40
42
|
setIsActive(Boolean(category.is_active))
|
|
41
43
|
setIsInternal(Boolean(category.is_internal))
|
|
42
44
|
setSelectedChannels(category.sales_channels || salesChannels.map((s) => s.id))
|
|
@@ -75,6 +77,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
75
77
|
name: name.trim(),
|
|
76
78
|
handle: handle.trim() || undefined,
|
|
77
79
|
description: description.trim() || undefined,
|
|
80
|
+
rank: Number(rank) || 0,
|
|
78
81
|
is_active: isActive,
|
|
79
82
|
is_internal: isInternal,
|
|
80
83
|
metadata: updatedMetadata,
|
|
@@ -86,6 +89,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
86
89
|
handle: handle.trim(),
|
|
87
90
|
description: description.trim() || null,
|
|
88
91
|
image_url: imageUrl.trim() || category.image_url,
|
|
92
|
+
rank: Number(rank) || 0,
|
|
89
93
|
sales_channels: selectedChannels,
|
|
90
94
|
is_active: isActive,
|
|
91
95
|
is_internal: isInternal,
|
|
@@ -156,6 +160,56 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
156
160
|
/>
|
|
157
161
|
</div>
|
|
158
162
|
|
|
163
|
+
<div className="flex flex-col gap-1.5 p-3 rounded-lg bg-ui-bg-subtle border border-ui-border-base">
|
|
164
|
+
<div className="flex items-center justify-between">
|
|
165
|
+
<Label htmlFor="edit-rank" className="text-xs font-semibold">
|
|
166
|
+
Storefront Ranking (Sort Order)
|
|
167
|
+
</Label>
|
|
168
|
+
<span className="text-[10px] font-mono text-ui-fg-muted">
|
|
169
|
+
Controls Storefront 2 & Monfil
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<Input
|
|
174
|
+
id="edit-rank"
|
|
175
|
+
type="number"
|
|
176
|
+
size="small"
|
|
177
|
+
value={rank}
|
|
178
|
+
onChange={(e) => setRank(Number(e.target.value))}
|
|
179
|
+
className="w-24"
|
|
180
|
+
/>
|
|
181
|
+
<div className="flex items-center gap-1">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => setRank((r) => Math.max(0, r - 1))}
|
|
185
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
186
|
+
title="Move Earlier in Storefront"
|
|
187
|
+
>
|
|
188
|
+
↑ Earlier
|
|
189
|
+
</button>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => setRank((r) => r + 1)}
|
|
193
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
194
|
+
title="Move Later in Storefront"
|
|
195
|
+
>
|
|
196
|
+
↓ Later
|
|
197
|
+
</button>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() => setRank(0)}
|
|
201
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
202
|
+
title="Set as First Category"
|
|
203
|
+
>
|
|
204
|
+
Top (#0)
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<p className="text-[11px] text-ui-fg-muted leading-relaxed">
|
|
209
|
+
Categories are sorted by rank ASC (0, 1, 2...). Lower numbers appear first in your storefront navigation.
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
159
213
|
<div className="flex flex-col gap-1.5">
|
|
160
214
|
<Label htmlFor="edit-image" className="text-xs font-semibold">
|
|
161
215
|
Category Image URL
|