@hectasquare/medusa-plugin-category-tree 1.0.0 → 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 +22 -13
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +43 -4
- 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/.medusa/server/src/admin/routes/category-tree/page.js +2 -1
- package/package.json +1 -1
- package/src/admin/components/category-tree/canvas-dock.tsx +93 -45
- package/src/admin/components/category-tree/category-node.tsx +77 -41
- package/src/admin/components/category-tree/category-tree-canvas.tsx +62 -6
- 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
- package/src/admin/routes/category-tree/page.tsx +6 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
export interface TierBarsProps {
|
|
3
3
|
tier: number;
|
|
4
|
+
color?: string;
|
|
4
5
|
}
|
|
5
6
|
/**
|
|
6
|
-
* Renders N vertical
|
|
7
|
-
*
|
|
7
|
+
* Renders N vertical colored bars on the left edge of each category card,
|
|
8
|
+
* providing an immediate tactile hierarchy indicator:
|
|
8
9
|
* Tier 1 = 1 bar (Root/Main)
|
|
9
10
|
* Tier 2 = 2 bars (Sub)
|
|
10
11
|
* Tier 3 = 3 bars (Sub-Sub)
|
|
11
12
|
* Tier N = N bars
|
|
13
|
+
* Bar color matches the branch lineage for instant visual identification.
|
|
12
14
|
*/
|
|
13
15
|
export declare const TierBars: React.FC<TierBarsProps>;
|
|
@@ -3,15 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TierBars = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
/**
|
|
6
|
-
* Renders N vertical
|
|
7
|
-
*
|
|
6
|
+
* Renders N vertical colored bars on the left edge of each category card,
|
|
7
|
+
* providing an immediate tactile hierarchy indicator:
|
|
8
8
|
* Tier 1 = 1 bar (Root/Main)
|
|
9
9
|
* Tier 2 = 2 bars (Sub)
|
|
10
10
|
* Tier 3 = 3 bars (Sub-Sub)
|
|
11
11
|
* Tier N = N bars
|
|
12
|
+
* Bar color matches the branch lineage for instant visual identification.
|
|
12
13
|
*/
|
|
13
|
-
const TierBars = ({ tier }) => {
|
|
14
|
+
const TierBars = ({ tier, color }) => {
|
|
14
15
|
const count = Math.max(1, tier);
|
|
15
|
-
|
|
16
|
+
const barColor = color || "var(--fg-interactive, #3b82f6)";
|
|
17
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex h-full py-2 pl-2 pr-1.5 gap-[2.5px] items-stretch flex-shrink-0 select-none bg-neutral-50 dark:bg-neutral-900/60 rounded-l-xl border-r border-ui-border-base/50", title: `Tier ${tier} (${tier === 1 ? "Main Root Category" : tier === 2 ? "Subcategory" : "Sub-subcategory"}) - Branch Color: ${barColor}`, children: Array.from({ length: count }).map((_, index) => ((0, jsx_runtime_1.jsx)("span", { className: "w-[3.5px] rounded-full shadow-xs transition-colors", style: {
|
|
18
|
+
backgroundColor: barColor,
|
|
19
|
+
boxShadow: `0 0 4px ${barColor}40`,
|
|
20
|
+
} }, index))) }));
|
|
16
21
|
};
|
|
17
22
|
exports.TierBars = TierBars;
|
|
@@ -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
|
};
|
|
@@ -4,9 +4,10 @@ exports.config = void 0;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const admin_sdk_1 = require("@medusajs/admin-sdk");
|
|
6
6
|
const icons_1 = require("@medusajs/icons");
|
|
7
|
+
const ui_1 = require("@medusajs/ui");
|
|
7
8
|
const category_tree_canvas_1 = require("../../components/category-tree/category-tree-canvas");
|
|
8
9
|
const CategoryTreePage = () => {
|
|
9
|
-
return (0, jsx_runtime_1.jsx)(category_tree_canvas_1.CategoryTreeCanvas, {});
|
|
10
|
+
return ((0, jsx_runtime_1.jsx)(ui_1.TooltipProvider, { children: (0, jsx_runtime_1.jsx)(category_tree_canvas_1.CategoryTreeCanvas, {}) }));
|
|
10
11
|
};
|
|
11
12
|
exports.config = (0, admin_sdk_1.defineRouteConfig)({
|
|
12
13
|
label: "Category Tree",
|
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { Button
|
|
2
|
+
import { Button } from "@medusajs/ui"
|
|
3
3
|
import {
|
|
4
4
|
ArrowsPointingOut,
|
|
5
5
|
MagnifyingGlass,
|
|
@@ -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,
|
|
@@ -74,18 +86,17 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
74
86
|
</div>
|
|
75
87
|
|
|
76
88
|
<div className="flex items-center border-r border-ui-border-base pr-2.5">
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
</Tooltip>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
title={direction === "TB" ? "Switch to Mind Map (Left-to-Right)" : "Switch to Vertical Tree (Top-to-Bottom)"}
|
|
92
|
+
onClick={onToggleDirection}
|
|
93
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg bg-ui-bg-subtle text-ui-fg-base hover:bg-ui-bg-base border border-ui-border-base transition-colors"
|
|
94
|
+
>
|
|
95
|
+
<span>{direction === "TB" ? "⬇ Vertical" : "➡ Mind Map"}</span>
|
|
96
|
+
</button>
|
|
86
97
|
</div>
|
|
87
98
|
|
|
88
|
-
<div className="relative w-
|
|
99
|
+
<div className="relative w-36 sm:w-44">
|
|
89
100
|
<input
|
|
90
101
|
type="text"
|
|
91
102
|
placeholder="Search categories…"
|
|
@@ -96,48 +107,85 @@ export const CanvasDock: React.FC<CanvasDockProps> = ({
|
|
|
96
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" />
|
|
97
108
|
</div>
|
|
98
109
|
|
|
110
|
+
{/* Expand / Collapse Section with Selective awareness */}
|
|
99
111
|
<div className="flex items-center gap-1 border-l border-ui-border-base pl-2.5">
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
)}
|
|
151
|
+
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
title="Focus on Root Categories"
|
|
155
|
+
onClick={onFocusRoot}
|
|
156
|
+
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"
|
|
157
|
+
>
|
|
158
|
+
Root
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
119
161
|
|
|
120
|
-
|
|
162
|
+
{/* Box Selection / Marquee mode toggle */}
|
|
163
|
+
{onToggleBoxSelect && (
|
|
164
|
+
<div className="flex items-center border-l border-ui-border-base pl-2.5">
|
|
121
165
|
<button
|
|
122
166
|
type="button"
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}`}
|
|
125
174
|
>
|
|
126
|
-
|
|
175
|
+
⛶ Box Select
|
|
127
176
|
</button>
|
|
128
|
-
</
|
|
129
|
-
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
130
179
|
|
|
131
180
|
<div className="flex items-center gap-1 border-l border-ui-border-base pl-2.5">
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</Tooltip>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
title="Fit View (Center)"
|
|
184
|
+
onClick={onFitView}
|
|
185
|
+
className="p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-md transition-colors"
|
|
186
|
+
>
|
|
187
|
+
<ArrowsPointingOut className="w-3.5 h-3.5" />
|
|
188
|
+
</button>
|
|
141
189
|
</div>
|
|
142
190
|
|
|
143
191
|
<div className="border-l border-ui-border-base pl-2.5">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { memo } from "react"
|
|
2
2
|
import { Handle, Position } from "@xyflow/react"
|
|
3
|
-
import { Text
|
|
3
|
+
import { Text } from "@medusajs/ui"
|
|
4
4
|
import { Plus, PencilSquare, Folder, ArrowUturnLeft } from "@medusajs/icons"
|
|
5
5
|
import { TierBars } from "./tier-bars"
|
|
6
6
|
import { CategoryNodeData } from "./types"
|
|
@@ -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
|
|
|
@@ -134,45 +173,42 @@ export const CategoryNode = memo(({ id, data, selected }: CategoryNodeProps) =>
|
|
|
134
173
|
</div>
|
|
135
174
|
|
|
136
175
|
<div className="absolute -top-3 right-2 hidden group-hover:flex items-center gap-1 px-1.5 py-0.5 bg-ui-bg-base/95 backdrop-blur-sm border border-ui-border-base rounded-full shadow-md z-20">
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
title="Add Subcategory"
|
|
179
|
+
onClick={(e) => {
|
|
180
|
+
e.stopPropagation()
|
|
181
|
+
onAddChild(category)
|
|
182
|
+
}}
|
|
183
|
+
className="p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-full transition-colors"
|
|
184
|
+
>
|
|
185
|
+
<Plus className="w-3.5 h-3.5" />
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
title="Edit Category"
|
|
191
|
+
onClick={(e) => {
|
|
192
|
+
e.stopPropagation()
|
|
193
|
+
onEdit(category)
|
|
194
|
+
}}
|
|
195
|
+
className="p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-full transition-colors"
|
|
196
|
+
>
|
|
197
|
+
<PencilSquare className="w-3.5 h-3.5" />
|
|
198
|
+
</button>
|
|
149
199
|
|
|
150
|
-
|
|
200
|
+
{category.parent_category_id && (
|
|
151
201
|
<button
|
|
152
202
|
type="button"
|
|
203
|
+
title="Promote to Root (Detach)"
|
|
153
204
|
onClick={(e) => {
|
|
154
205
|
e.stopPropagation()
|
|
155
|
-
|
|
206
|
+
onDetach(id)
|
|
156
207
|
}}
|
|
157
|
-
className="p-1 text-ui-fg-subtle hover:text-
|
|
208
|
+
className="p-1 text-ui-fg-subtle hover:text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-950/50 rounded-full transition-colors"
|
|
158
209
|
>
|
|
159
|
-
<
|
|
210
|
+
<ArrowUturnLeft className="w-3.5 h-3.5" />
|
|
160
211
|
</button>
|
|
161
|
-
</Tooltip>
|
|
162
|
-
|
|
163
|
-
{category.parent_category_id && (
|
|
164
|
-
<Tooltip content="Promote to Root (Detach)">
|
|
165
|
-
<button
|
|
166
|
-
type="button"
|
|
167
|
-
onClick={(e) => {
|
|
168
|
-
e.stopPropagation()
|
|
169
|
-
onDetach(id)
|
|
170
|
-
}}
|
|
171
|
-
className="p-1 text-ui-fg-subtle hover:text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-950/50 rounded-full transition-colors"
|
|
172
|
-
>
|
|
173
|
-
<ArrowUturnLeft className="w-3.5 h-3.5" />
|
|
174
|
-
</button>
|
|
175
|
-
</Tooltip>
|
|
176
212
|
)}
|
|
177
213
|
</div>
|
|
178
214
|
|
|
@@ -11,9 +11,10 @@ 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
|
-
import { toast } from "@medusajs/ui"
|
|
17
|
+
import { toast, TooltipProvider } from "@medusajs/ui"
|
|
17
18
|
|
|
18
19
|
import { CategoryNode } from "./category-node"
|
|
19
20
|
import { CustomSmoothStepEdge } from "./custom-edge"
|
|
@@ -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}
|
|
@@ -390,8 +444,10 @@ const CanvasInner: React.FC = () => {
|
|
|
390
444
|
|
|
391
445
|
export const CategoryTreeCanvas: React.FC = () => {
|
|
392
446
|
return (
|
|
393
|
-
<
|
|
394
|
-
<
|
|
395
|
-
|
|
447
|
+
<TooltipProvider>
|
|
448
|
+
<ReactFlowProvider>
|
|
449
|
+
<CanvasInner />
|
|
450
|
+
</ReactFlowProvider>
|
|
451
|
+
</TooltipProvider>
|
|
396
452
|
)
|
|
397
453
|
}
|