@hectasquare/medusa-plugin-category-tree 1.0.1 → 1.0.5
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-products-manager.d.ts +8 -0
- package/.medusa/server/src/admin/components/category-tree/category-products-manager.js +290 -0
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +45 -3
- package/.medusa/server/src/admin/components/category-tree/custom-edge.js +5 -3
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +89 -8
- 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 +124 -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-products-manager.tsx +749 -0
- package/src/admin/components/category-tree/category-tree-canvas.tsx +59 -2
- package/src/admin/components/category-tree/custom-edge.tsx +6 -2
- package/src/admin/components/category-tree/edit-category-drawer.tsx +413 -118
- package/src/admin/components/category-tree/lib/layout-engine.ts +161 -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
|
@@ -5,15 +5,40 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const ui_1 = require("@medusajs/ui");
|
|
7
7
|
const icons_1 = require("@medusajs/icons");
|
|
8
|
+
const category_products_manager_1 = require("./category-products-manager");
|
|
8
9
|
const sdk_1 = require("../../lib/sdk");
|
|
10
|
+
function extractYouTubeVideoId(url) {
|
|
11
|
+
if (!url || typeof url !== "string")
|
|
12
|
+
return null;
|
|
13
|
+
const trimmed = url.trim();
|
|
14
|
+
const patterns = [
|
|
15
|
+
/(?:youtube\.com\/shorts\/)([^?&\s]+)/i,
|
|
16
|
+
/(?:youtube\.com\/watch\?.*v=)([^?&\s]+)/i,
|
|
17
|
+
/(?:youtu\.be\/)([^?&\s]+)/i,
|
|
18
|
+
/(?:youtube\.com\/embed\/)([^?&\s]+)/i,
|
|
19
|
+
];
|
|
20
|
+
for (const pattern of patterns) {
|
|
21
|
+
const match = trimmed.match(pattern);
|
|
22
|
+
if (match && match[1])
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed))
|
|
26
|
+
return trimmed;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
9
29
|
const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuccess, onDeleteSuccess, }) => {
|
|
30
|
+
const [activeTab, setActiveTab] = (0, react_1.useState)("general");
|
|
10
31
|
const [name, setName] = (0, react_1.useState)("");
|
|
11
32
|
const [handle, setHandle] = (0, react_1.useState)("");
|
|
12
33
|
const [description, setDescription] = (0, react_1.useState)("");
|
|
13
34
|
const [imageUrl, setImageUrl] = (0, react_1.useState)("");
|
|
35
|
+
const [rank, setRank] = (0, react_1.useState)(0);
|
|
14
36
|
const [isActive, setIsActive] = (0, react_1.useState)(true);
|
|
15
37
|
const [isInternal, setIsInternal] = (0, react_1.useState)(false);
|
|
16
38
|
const [selectedChannels, setSelectedChannels] = (0, react_1.useState)([]);
|
|
39
|
+
const [youtubeUrls, setYoutubeUrls] = (0, react_1.useState)([]);
|
|
40
|
+
const [newYoutubeInput, setNewYoutubeInput] = (0, react_1.useState)("");
|
|
41
|
+
const [currentMetadata, setCurrentMetadata] = (0, react_1.useState)({});
|
|
17
42
|
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
18
43
|
const [isDeleting, setIsDeleting] = (0, react_1.useState)(false);
|
|
19
44
|
(0, react_1.useEffect)(() => {
|
|
@@ -22,9 +47,22 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
22
47
|
setHandle(category.handle || "");
|
|
23
48
|
setDescription(category.description || "");
|
|
24
49
|
setImageUrl(category.image_url || "");
|
|
50
|
+
setRank(category.rank ?? 0);
|
|
25
51
|
setIsActive(Boolean(category.is_active));
|
|
26
52
|
setIsInternal(Boolean(category.is_internal));
|
|
27
53
|
setSelectedChannels(category.sales_channels || salesChannels.map((s) => s.id));
|
|
54
|
+
setCurrentMetadata(category.metadata || {});
|
|
55
|
+
// Parse YouTube URLs from metadata
|
|
56
|
+
const meta = category.metadata;
|
|
57
|
+
const urls = [];
|
|
58
|
+
if (meta?.youtube_urls && Array.isArray(meta.youtube_urls)) {
|
|
59
|
+
meta.youtube_urls.forEach((u) => typeof u === "string" && u.trim() && urls.push(u.trim()));
|
|
60
|
+
}
|
|
61
|
+
else if (meta?.youtube_url && typeof meta.youtube_url === "string") {
|
|
62
|
+
urls.push(meta.youtube_url.trim());
|
|
63
|
+
}
|
|
64
|
+
setYoutubeUrls(urls);
|
|
65
|
+
setActiveTab("general");
|
|
28
66
|
}
|
|
29
67
|
}, [category, salesChannels]);
|
|
30
68
|
if (!category)
|
|
@@ -34,6 +72,27 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
34
72
|
? prev.filter((id) => id !== channelId)
|
|
35
73
|
: [...prev, channelId]);
|
|
36
74
|
};
|
|
75
|
+
const handleAddYoutube = () => {
|
|
76
|
+
const raw = newYoutubeInput.trim();
|
|
77
|
+
if (!raw)
|
|
78
|
+
return;
|
|
79
|
+
const id = extractYouTubeVideoId(raw);
|
|
80
|
+
if (!id) {
|
|
81
|
+
ui_1.toast.error("Please enter a valid YouTube URL or video ID");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const cleanUrl = `https://www.youtube.com/watch?v=${id}`;
|
|
85
|
+
if (youtubeUrls.includes(cleanUrl)) {
|
|
86
|
+
ui_1.toast.error("Video already added");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setYoutubeUrls((prev) => [...prev, cleanUrl]);
|
|
90
|
+
setNewYoutubeInput("");
|
|
91
|
+
ui_1.toast.success("YouTube video added");
|
|
92
|
+
};
|
|
93
|
+
const handleRemoveYoutube = (urlToRemove) => {
|
|
94
|
+
setYoutubeUrls((prev) => prev.filter((u) => u !== urlToRemove));
|
|
95
|
+
};
|
|
37
96
|
const handleSubmit = async (e) => {
|
|
38
97
|
e.preventDefault();
|
|
39
98
|
if (!name.trim()) {
|
|
@@ -43,8 +102,9 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
43
102
|
setIsSubmitting(true);
|
|
44
103
|
try {
|
|
45
104
|
const updatedMetadata = {
|
|
46
|
-
...(
|
|
105
|
+
...(currentMetadata || {}),
|
|
47
106
|
sales_channels: selectedChannels,
|
|
107
|
+
youtube_urls: youtubeUrls,
|
|
48
108
|
};
|
|
49
109
|
if (imageUrl.trim()) {
|
|
50
110
|
updatedMetadata.thumbnail = imageUrl.trim();
|
|
@@ -54,6 +114,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
54
114
|
name: name.trim(),
|
|
55
115
|
handle: handle.trim() || undefined,
|
|
56
116
|
description: description.trim() || undefined,
|
|
117
|
+
rank: Number(rank) || 0,
|
|
57
118
|
is_active: isActive,
|
|
58
119
|
is_internal: isInternal,
|
|
59
120
|
metadata: updatedMetadata,
|
|
@@ -64,6 +125,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
64
125
|
handle: handle.trim(),
|
|
65
126
|
description: description.trim() || null,
|
|
66
127
|
image_url: imageUrl.trim() || category.image_url,
|
|
128
|
+
rank: Number(rank) || 0,
|
|
67
129
|
sales_channels: selectedChannels,
|
|
68
130
|
is_active: isActive,
|
|
69
131
|
is_internal: isInternal,
|
|
@@ -98,12 +160,31 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
98
160
|
setIsDeleting(false);
|
|
99
161
|
}
|
|
100
162
|
};
|
|
101
|
-
return ((0, jsx_runtime_1.jsx)(ui_1.Drawer, { open: open, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(ui_1.Drawer.Content, { className: "max-w-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
163
|
+
return ((0, jsx_runtime_1.jsx)(ui_1.Drawer, { open: open, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(ui_1.Drawer.Content, { className: "max-w-3xl w-full", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col h-full", children: [(0, jsx_runtime_1.jsxs)(ui_1.Drawer.Header, { className: "border-b border-ui-border-base pb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between w-full", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Drawer.Title, { className: "text-base font-semibold text-ui-fg-base", children: category.name }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Description, { className: "text-xs text-ui-fg-muted mt-0.5", children: ["Tier ", category.tier, " \u2022 ID: ", category.id] })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => window.open(`/app/categories/${category.id}`, "_blank"), className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors", title: "Open full category details page in a new tab", children: [(0, jsx_runtime_1.jsx)("span", { children: "Open Full Page" }), (0, jsx_runtime_1.jsx)(icons_1.ArrowUpRightMicro, { className: "w-3.5 h-3.5" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 mt-3 border-b border-ui-border-base/50 -mb-3 pb-1", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("general"), className: `px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${activeTab === "general"
|
|
164
|
+
? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
|
|
165
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "General & Ranking" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("products"), className: `px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${activeTab === "products"
|
|
166
|
+
? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
|
|
167
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Products Management" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("channels"), className: `px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${activeTab === "channels"
|
|
168
|
+
? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
|
|
169
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Storefronts & Media" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setActiveTab("youtube"), className: `px-3 py-1.5 text-xs font-medium rounded-t-lg transition-colors border-b-2 -mb-[5px] ${activeTab === "youtube"
|
|
170
|
+
? "border-ui-border-interactive text-ui-fg-base bg-ui-bg-subtle/50"
|
|
171
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: ["YouTube Videos (", youtubeUrls.length, ")"] })] })] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Body, { className: "flex flex-col gap-4 overflow-y-auto px-6 py-4 flex-1", children: [activeTab === "general" && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-name", className: "text-xs font-semibold", children: "Category Name *" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-name", size: "small", value: name, onChange: (e) => setName(e.target.value) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-handle", className: "text-xs font-semibold", children: "Handle (URL slug)" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-handle", size: "small", value: handle, onChange: (e) => setHandle(e.target.value) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 p-3 rounded-lg bg-ui-bg-subtle border border-ui-border-base", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-rank", className: "text-xs font-semibold", children: "Storefront Ranking (Sort Order)" }), (0, jsx_runtime_1.jsx)("span", { className: "text-[10px] font-mono text-ui-fg-muted", children: "Controls Storefront 2 & Monfil" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-rank", type: "number", size: "small", value: rank, onChange: (e) => setRank(Number(e.target.value)), className: "w-24" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank((r) => Math.max(0, r - 1)), 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", title: "Move Earlier in Storefront", children: "\u2191 Earlier" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank((r) => r + 1), 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", title: "Move Later in Storefront", children: "\u2193 Later" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank(0), 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", title: "Set as First Category", children: "Top (#0)" })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-[11px] text-ui-fg-muted leading-relaxed", children: "Categories are sorted by rank ASC (0, 1, 2...). Lower numbers appear first on your storefront navigation menu." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-desc", className: "text-xs font-semibold", children: "Description" }), (0, jsx_runtime_1.jsx)(ui_1.Textarea, { id: "edit-desc", value: description, onChange: (e) => setDescription(e.target.value), rows: 3 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between pt-2 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Active Status" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Visible to shoppers on enabled storefronts" })] }), (0, jsx_runtime_1.jsx)(ui_1.Switch, { checked: isActive, onCheckedChange: setIsActive })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Internal Only" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Hidden from public storefront navigation" })] }), (0, jsx_runtime_1.jsx)(ui_1.Switch, { checked: isInternal, onCheckedChange: setIsInternal })] }), (0, jsx_runtime_1.jsx)("div", { className: "pt-3 border-t border-ui-border-base", children: (0, jsx_runtime_1.jsxs)(ui_1.Button, { type: "button", variant: "danger", size: "small", onClick: handleDelete, isLoading: isDeleting, className: "w-full flex items-center justify-center gap-1.5", children: [(0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Delete Category" })] }) })] })), activeTab === "products" && ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-3", children: (0, jsx_runtime_1.jsx)(category_products_manager_1.CategoryProductsManager, { categoryId: category.id, categoryName: category.name, initialMetadata: currentMetadata, onMetadataUpdate: (meta) => {
|
|
172
|
+
setCurrentMetadata(meta);
|
|
173
|
+
} }) })), activeTab === "channels" && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Storefront Visibility" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Designate which storefront sales channels display this category." }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2 mt-1", children: salesChannels.map((channel) => {
|
|
174
|
+
const isChecked = selectedChannels.includes(channel.id);
|
|
175
|
+
return ((0, jsx_runtime_1.jsxs)("label", { className: "flex items-center gap-2.5 text-xs text-ui-fg-base cursor-pointer select-none p-2 rounded-lg border border-ui-border-base bg-ui-bg-subtle/40 hover:bg-ui-bg-subtle", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: isChecked, onChange: () => toggleChannel(channel.id), className: "rounded border-ui-border-base text-blue-600 focus:ring-blue-500 cursor-pointer" }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: channel.name })] }, channel.id));
|
|
176
|
+
}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 pt-3 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-image", className: "text-xs font-semibold", children: "Category Thumbnail / Image URL" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-image", size: "small", value: imageUrl, onChange: (e) => setImageUrl(e.target.value), placeholder: "https://example.com/category-image.jpg" }), imageUrl && ((0, jsx_runtime_1.jsx)("div", { className: "mt-2 w-24 h-24 rounded-xl overflow-hidden border border-ui-border-base bg-ui-bg-subtle shadow-xs", children: (0, jsx_runtime_1.jsx)("img", { src: imageUrl, alt: "Category Preview", className: "w-full h-full object-cover", onError: (e) => {
|
|
177
|
+
;
|
|
178
|
+
e.target.style.display = "none";
|
|
179
|
+
} }) }))] })] })), activeTab === "youtube" && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h4", { className: "text-xs font-semibold text-ui-fg-base", children: "Category Demonstration Videos (YouTube)" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-ui-fg-muted mt-0.5", children: "Attach YouTube video demonstration links to show interactive video carousels on the storefront category page." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Input, { size: "small", placeholder: "Paste YouTube watch URL, shorts, or video ID\u2026", value: newYoutubeInput, onChange: (e) => setNewYoutubeInput(e.target.value), className: "flex-1 text-xs", onKeyDown: (e) => {
|
|
180
|
+
if (e.key === "Enter") {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
handleAddYoutube();
|
|
183
|
+
}
|
|
184
|
+
} }), (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", type: "button", variant: "secondary", onClick: handleAddYoutube, className: "flex items-center gap-1 text-xs flex-shrink-0", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Add Video" })] })] }), youtubeUrls.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "p-8 text-center text-xs text-ui-fg-muted border border-dashed border-ui-border-base rounded-xl", children: "No demonstration videos attached yet." })) : ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 gap-3", children: youtubeUrls.map((url) => {
|
|
185
|
+
const id = extractYouTubeVideoId(url);
|
|
186
|
+
const thumb = id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null;
|
|
187
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-subtle/40 shadow-xs", children: [(0, jsx_runtime_1.jsxs)("div", { className: "relative aspect-video bg-black/80 overflow-hidden flex items-center justify-center", children: [thumb ? ((0, jsx_runtime_1.jsx)("img", { src: thumb, alt: "YouTube Preview", className: "w-full h-full object-cover" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-xs text-white", children: "Video" })), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => handleRemoveYoutube(url), className: "absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors", title: "Remove video", children: (0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3 h-3" }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-2", children: (0, jsx_runtime_1.jsx)("span", { className: "text-[10px] text-ui-fg-muted truncate block font-mono", title: url, children: url }) })] }, url));
|
|
188
|
+
}) }))] }))] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Footer, { className: "flex justify-end gap-2 px-6 py-4 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsx)(ui_1.Button, { type: "button", variant: "secondary", size: "small", onClick: () => onOpenChange(false), disabled: isSubmitting || isDeleting, children: "Close" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { type: "submit", size: "small", isLoading: isSubmitting, disabled: isSubmitting || !name.trim() || isDeleting, children: "Save Category" })] })] }) }) }));
|
|
108
189
|
};
|
|
109
190
|
exports.EditCategoryDrawer = EditCategoryDrawer;
|
|
@@ -6,7 +6,15 @@ export interface LayoutOptions {
|
|
|
6
6
|
nodeSep?: number;
|
|
7
7
|
rankSep?: number;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* AAA Clustered Hierarchical Tree Layout (Buchheim/Walker style)
|
|
11
|
+
* - Guarantees each parent is positioned at the exact midpoint/center of its children
|
|
12
|
+
* - Guarantees subtrees never overlap and stay tightly clustered
|
|
13
|
+
* - Completely eliminates long spanning cross-canvas threads
|
|
14
|
+
* - Sibling order is preserved strictly according to category rank
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeTreeLayout(nodes: Node[], edges: Edge[], options?: LayoutOptions): {
|
|
10
17
|
nodes: Node[];
|
|
11
18
|
edges: Edge[];
|
|
12
19
|
};
|
|
20
|
+
export declare const computeDagreLayout: typeof computeTreeLayout;
|
|
@@ -1,44 +1,138 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CARD_HEIGHT = exports.CARD_WIDTH = void 0;
|
|
7
|
-
exports.
|
|
8
|
-
const dagre_1 = __importDefault(require("@dagrejs/dagre"));
|
|
3
|
+
exports.computeDagreLayout = exports.CARD_HEIGHT = exports.CARD_WIDTH = void 0;
|
|
4
|
+
exports.computeTreeLayout = computeTreeLayout;
|
|
9
5
|
const react_1 = require("@xyflow/react");
|
|
10
6
|
exports.CARD_WIDTH = 270;
|
|
11
7
|
exports.CARD_HEIGHT = 86;
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* AAA Clustered Hierarchical Tree Layout (Buchheim/Walker style)
|
|
10
|
+
* - Guarantees each parent is positioned at the exact midpoint/center of its children
|
|
11
|
+
* - Guarantees subtrees never overlap and stay tightly clustered
|
|
12
|
+
* - Completely eliminates long spanning cross-canvas threads
|
|
13
|
+
* - Sibling order is preserved strictly according to category rank
|
|
14
|
+
*/
|
|
15
|
+
function computeTreeLayout(nodes, edges, options = {}) {
|
|
16
|
+
if (nodes.length === 0)
|
|
17
|
+
return { nodes: [], edges: [] };
|
|
13
18
|
const direction = options.direction || "TB";
|
|
14
19
|
const isTB = direction === "TB";
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
align: "UL",
|
|
22
|
-
ranker: "network-simplex",
|
|
23
|
-
});
|
|
24
|
-
nodes.forEach((node) => {
|
|
25
|
-
g.setNode(node.id, { width: exports.CARD_WIDTH, height: exports.CARD_HEIGHT });
|
|
26
|
-
});
|
|
20
|
+
const nodeSep = options.nodeSep ?? (isTB ? 40 : 28);
|
|
21
|
+
const rankSep = options.rankSep ?? (isTB ? 70 : 80);
|
|
22
|
+
const treeGap = isTB ? 80 : 64;
|
|
23
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
24
|
+
const childrenMap = new Map();
|
|
25
|
+
const parentMap = new Map();
|
|
27
26
|
edges.forEach((edge) => {
|
|
28
|
-
|
|
27
|
+
if (nodeMap.has(edge.source) && nodeMap.has(edge.target)) {
|
|
28
|
+
const list = childrenMap.get(edge.source) || [];
|
|
29
|
+
list.push(edge.target);
|
|
30
|
+
childrenMap.set(edge.source, list);
|
|
31
|
+
parentMap.set(edge.target, edge.source);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// Identify roots (nodes with no incoming edge in the visible graph)
|
|
35
|
+
const rootNodes = nodes.filter((n) => !parentMap.has(n.id));
|
|
36
|
+
// Sort children by their category rank
|
|
37
|
+
const getRank = (id) => {
|
|
38
|
+
const raw = nodeMap.get(id)?.data?.category?.rank;
|
|
39
|
+
return typeof raw === "number" ? raw : 0;
|
|
40
|
+
};
|
|
41
|
+
function buildTree(id, depth) {
|
|
42
|
+
const rawNode = nodeMap.get(id);
|
|
43
|
+
const childIds = (childrenMap.get(id) || []).sort((a, b) => getRank(a) - getRank(b) || a.localeCompare(b));
|
|
44
|
+
const children = childIds.map((childId) => buildTree(childId, depth + 1));
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
node: rawNode,
|
|
48
|
+
children,
|
|
49
|
+
depth,
|
|
50
|
+
span: 0,
|
|
51
|
+
primaryCoord: 0,
|
|
52
|
+
secondaryCoord: 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// 1. Post-order pass: Compute subtree span (width in TB, height in LR)
|
|
56
|
+
function computeSpan(treeNode) {
|
|
57
|
+
const baseSpan = isTB ? exports.CARD_WIDTH : exports.CARD_HEIGHT;
|
|
58
|
+
if (treeNode.children.length === 0) {
|
|
59
|
+
treeNode.span = baseSpan;
|
|
60
|
+
return baseSpan;
|
|
61
|
+
}
|
|
62
|
+
let childrenSpanTotal = 0;
|
|
63
|
+
treeNode.children.forEach((child, index) => {
|
|
64
|
+
const childSpan = computeSpan(child);
|
|
65
|
+
childrenSpanTotal += childSpan + (index > 0 ? nodeSep : 0);
|
|
66
|
+
});
|
|
67
|
+
treeNode.span = Math.max(baseSpan, childrenSpanTotal);
|
|
68
|
+
return treeNode.span;
|
|
69
|
+
}
|
|
70
|
+
// 2. Pre-order pass: Position nodes
|
|
71
|
+
function assignPositions(treeNode, startOffset, depth) {
|
|
72
|
+
const baseSpan = isTB ? exports.CARD_WIDTH : exports.CARD_HEIGHT;
|
|
73
|
+
const baseSecondary = isTB ? exports.CARD_HEIGHT : exports.CARD_WIDTH;
|
|
74
|
+
treeNode.secondaryCoord = depth * (baseSecondary + rankSep);
|
|
75
|
+
if (treeNode.children.length === 0) {
|
|
76
|
+
treeNode.primaryCoord = startOffset + (treeNode.span - baseSpan) / 2;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let currentOffset = startOffset;
|
|
80
|
+
treeNode.children.forEach((child) => {
|
|
81
|
+
assignPositions(child, currentOffset, depth + 1);
|
|
82
|
+
currentOffset += child.span + nodeSep;
|
|
83
|
+
});
|
|
84
|
+
// Center parent directly between first and last child
|
|
85
|
+
const firstChild = treeNode.children[0];
|
|
86
|
+
const lastChild = treeNode.children[treeNode.children.length - 1];
|
|
87
|
+
const centerOfChildren = (firstChild.primaryCoord + lastChild.primaryCoord) / 2;
|
|
88
|
+
treeNode.primaryCoord = centerOfChildren;
|
|
89
|
+
}
|
|
90
|
+
// Sort roots by their rank
|
|
91
|
+
rootNodes.sort((a, b) => getRank(a.id) - getRank(b.id) || a.id.localeCompare(b.id));
|
|
92
|
+
const positionedNodes = [];
|
|
93
|
+
let globalOffset = 0;
|
|
94
|
+
rootNodes.forEach((root) => {
|
|
95
|
+
const tree = buildTree(root.id, 0);
|
|
96
|
+
computeSpan(tree);
|
|
97
|
+
assignPositions(tree, globalOffset, 0);
|
|
98
|
+
globalOffset += tree.span + treeGap;
|
|
99
|
+
// Flatten tree
|
|
100
|
+
function collect(node) {
|
|
101
|
+
const x = isTB ? node.primaryCoord : node.secondaryCoord;
|
|
102
|
+
const y = isTB ? node.secondaryCoord : node.primaryCoord;
|
|
103
|
+
positionedNodes.push({
|
|
104
|
+
...node.node,
|
|
105
|
+
targetPosition: isTB ? react_1.Position.Top : react_1.Position.Left,
|
|
106
|
+
sourcePosition: isTB ? react_1.Position.Bottom : react_1.Position.Right,
|
|
107
|
+
position: { x, y },
|
|
108
|
+
});
|
|
109
|
+
node.children.forEach(collect);
|
|
110
|
+
}
|
|
111
|
+
collect(tree);
|
|
29
112
|
});
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
113
|
+
// Style edges to match branch color
|
|
114
|
+
const styledEdges = edges.map((e) => {
|
|
115
|
+
const sourceNode = nodeMap.get(e.source);
|
|
116
|
+
const targetNode = nodeMap.get(e.target);
|
|
117
|
+
const branchColor = e.data?.branchColor ||
|
|
118
|
+
targetNode?.data?.branchColor ||
|
|
119
|
+
sourceNode?.data?.branchColor ||
|
|
120
|
+
"#64748b";
|
|
33
121
|
return {
|
|
34
|
-
...
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
122
|
+
...e,
|
|
123
|
+
data: {
|
|
124
|
+
...e.data,
|
|
125
|
+
branchColor,
|
|
126
|
+
},
|
|
127
|
+
style: {
|
|
128
|
+
...e.style,
|
|
129
|
+
stroke: branchColor,
|
|
130
|
+
strokeWidth: 2,
|
|
131
|
+
opacity: 0.95,
|
|
40
132
|
},
|
|
41
133
|
};
|
|
42
134
|
});
|
|
43
|
-
return { nodes:
|
|
135
|
+
return { nodes: positionedNodes, edges: styledEdges };
|
|
44
136
|
}
|
|
137
|
+
// Keep computeDagreLayout as alias to guarantee backwards compatibility
|
|
138
|
+
exports.computeDagreLayout = computeTreeLayout;
|
|
@@ -13,3 +13,17 @@ export declare function getVisibleTree(allNodes: Node[], allEdges: Edge[], colla
|
|
|
13
13
|
visibleEdges: Edge[];
|
|
14
14
|
};
|
|
15
15
|
export declare function computeMoves(original: CategoryTreeNode[], current: CategoryTreeNode[]): CategoryMoveItem[];
|
|
16
|
+
export declare const BRANCH_PALETTE: {
|
|
17
|
+
name: string;
|
|
18
|
+
hex: string;
|
|
19
|
+
}[];
|
|
20
|
+
/**
|
|
21
|
+
* Assigns a deterministic branch color to each category based on its top-level root ancestor.
|
|
22
|
+
* All descendants in a root branch share the exact same branch color.
|
|
23
|
+
*/
|
|
24
|
+
export declare function computeBranchColors(categories: CategoryTreeNode[]): Map<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Swaps sibling ranking (up or down). Modifies rank property among siblings
|
|
27
|
+
* and returns a new categories array ready for preview and commit.
|
|
28
|
+
*/
|
|
29
|
+
export declare function moveSiblingRank(categories: CategoryTreeNode[], targetId: string, direction: "up" | "down"): CategoryTreeNode[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BRANCH_PALETTE = void 0;
|
|
3
4
|
exports.toSuperscript = toSuperscript;
|
|
4
5
|
exports.computeTiers = computeTiers;
|
|
5
6
|
exports.computeSiblingRanks = computeSiblingRanks;
|
|
@@ -7,6 +8,8 @@ exports.wouldCreateCycle = wouldCreateCycle;
|
|
|
7
8
|
exports.computeSubtreeIds = computeSubtreeIds;
|
|
8
9
|
exports.getVisibleTree = getVisibleTree;
|
|
9
10
|
exports.computeMoves = computeMoves;
|
|
11
|
+
exports.computeBranchColors = computeBranchColors;
|
|
12
|
+
exports.moveSiblingRank = moveSiblingRank;
|
|
10
13
|
const SUPERSCRIPT_MAP = {
|
|
11
14
|
"0": "⁰",
|
|
12
15
|
"1": "¹",
|
|
@@ -127,3 +130,95 @@ function computeMoves(original, current) {
|
|
|
127
130
|
}
|
|
128
131
|
return moves;
|
|
129
132
|
}
|
|
133
|
+
exports.BRANCH_PALETTE = [
|
|
134
|
+
{ name: "Emerald", hex: "#10b981" },
|
|
135
|
+
{ name: "Indigo", hex: "#6366f1" },
|
|
136
|
+
{ name: "Amber", hex: "#f59e0b" },
|
|
137
|
+
{ name: "Rose", hex: "#f43f5e" },
|
|
138
|
+
{ name: "Cyan", hex: "#06b6d4" },
|
|
139
|
+
{ name: "Violet", hex: "#8b5cf6" },
|
|
140
|
+
{ name: "Pink", hex: "#ec4899" },
|
|
141
|
+
{ name: "Lime", hex: "#84cc16" },
|
|
142
|
+
{ name: "Sky", hex: "#0ea5e9" },
|
|
143
|
+
{ name: "Orange", hex: "#f97316" },
|
|
144
|
+
{ name: "Teal", hex: "#14b8a6" },
|
|
145
|
+
{ name: "Purple", hex: "#a855f7" },
|
|
146
|
+
];
|
|
147
|
+
/**
|
|
148
|
+
* Assigns a deterministic branch color to each category based on its top-level root ancestor.
|
|
149
|
+
* All descendants in a root branch share the exact same branch color.
|
|
150
|
+
*/
|
|
151
|
+
function computeBranchColors(categories) {
|
|
152
|
+
const catMap = new Map(categories.map((c) => [c.id, c]));
|
|
153
|
+
const rootMap = new Map();
|
|
154
|
+
function findRootId(id, visited) {
|
|
155
|
+
if (rootMap.has(id))
|
|
156
|
+
return rootMap.get(id);
|
|
157
|
+
if (visited.has(id))
|
|
158
|
+
return id;
|
|
159
|
+
visited.add(id);
|
|
160
|
+
const cat = catMap.get(id);
|
|
161
|
+
if (!cat || !cat.parent_category_id || !catMap.has(cat.parent_category_id)) {
|
|
162
|
+
rootMap.set(id, id);
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
const root = findRootId(cat.parent_category_id, visited);
|
|
166
|
+
rootMap.set(id, root);
|
|
167
|
+
return root;
|
|
168
|
+
}
|
|
169
|
+
for (const c of categories) {
|
|
170
|
+
findRootId(c.id, new Set());
|
|
171
|
+
}
|
|
172
|
+
const rootIds = Array.from(new Set(categories.filter((c) => !c.parent_category_id).map((c) => c.id))).sort((a, b) => {
|
|
173
|
+
const catA = catMap.get(a);
|
|
174
|
+
const catB = catMap.get(b);
|
|
175
|
+
return (catA?.rank ?? 0) - (catB?.rank ?? 0) || a.localeCompare(b);
|
|
176
|
+
});
|
|
177
|
+
const rootColorMap = new Map();
|
|
178
|
+
rootIds.forEach((id, index) => {
|
|
179
|
+
rootColorMap.set(id, exports.BRANCH_PALETTE[index % exports.BRANCH_PALETTE.length].hex);
|
|
180
|
+
});
|
|
181
|
+
const result = new Map();
|
|
182
|
+
for (const c of categories) {
|
|
183
|
+
const rootId = rootMap.get(c.id) || c.id;
|
|
184
|
+
const color = rootColorMap.get(rootId) || exports.BRANCH_PALETTE[0].hex;
|
|
185
|
+
result.set(c.id, color);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Swaps sibling ranking (up or down). Modifies rank property among siblings
|
|
191
|
+
* and returns a new categories array ready for preview and commit.
|
|
192
|
+
*/
|
|
193
|
+
function moveSiblingRank(categories, targetId, direction) {
|
|
194
|
+
const target = categories.find((c) => c.id === targetId);
|
|
195
|
+
if (!target)
|
|
196
|
+
return categories;
|
|
197
|
+
const parentId = target.parent_category_id || null;
|
|
198
|
+
const siblings = categories
|
|
199
|
+
.filter((c) => (c.parent_category_id || null) === parentId)
|
|
200
|
+
.sort((a, b) => a.rank - b.rank);
|
|
201
|
+
const currentIndex = siblings.findIndex((s) => s.id === targetId);
|
|
202
|
+
if (currentIndex === -1)
|
|
203
|
+
return categories;
|
|
204
|
+
const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
|
|
205
|
+
if (targetIndex < 0 || targetIndex >= siblings.length)
|
|
206
|
+
return categories;
|
|
207
|
+
const adjacent = siblings[targetIndex];
|
|
208
|
+
// Normalize ranks if they are identical or uninitialized
|
|
209
|
+
const updatedRanks = new Map();
|
|
210
|
+
siblings.forEach((s, idx) => {
|
|
211
|
+
updatedRanks.set(s.id, idx * 10);
|
|
212
|
+
});
|
|
213
|
+
// Swap target and adjacent
|
|
214
|
+
const targetNewRank = targetIndex * 10;
|
|
215
|
+
const adjacentNewRank = currentIndex * 10;
|
|
216
|
+
updatedRanks.set(target.id, targetNewRank);
|
|
217
|
+
updatedRanks.set(adjacent.id, adjacentNewRank);
|
|
218
|
+
return categories.map((c) => {
|
|
219
|
+
if (updatedRanks.has(c.id)) {
|
|
220
|
+
return { ...c, rank: updatedRanks.get(c.id) };
|
|
221
|
+
}
|
|
222
|
+
return c;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -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
|
};
|
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.5",
|
|
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",
|