@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.
Files changed (26) hide show
  1. package/.medusa/server/src/admin/components/category-tree/canvas-dock.d.ts +5 -0
  2. package/.medusa/server/src/admin/components/category-tree/canvas-dock.js +4 -2
  3. package/.medusa/server/src/admin/components/category-tree/category-node.js +12 -2
  4. package/.medusa/server/src/admin/components/category-tree/category-products-manager.d.ts +8 -0
  5. package/.medusa/server/src/admin/components/category-tree/category-products-manager.js +290 -0
  6. package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +45 -3
  7. package/.medusa/server/src/admin/components/category-tree/custom-edge.js +5 -3
  8. package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +89 -8
  9. package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.d.ts +9 -1
  10. package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.js +124 -30
  11. package/.medusa/server/src/admin/components/category-tree/lib/tree-state.d.ts +14 -0
  12. package/.medusa/server/src/admin/components/category-tree/lib/tree-state.js +95 -0
  13. package/.medusa/server/src/admin/components/category-tree/tier-bars.d.ts +4 -2
  14. package/.medusa/server/src/admin/components/category-tree/tier-bars.js +9 -4
  15. package/.medusa/server/src/admin/components/category-tree/types.d.ts +4 -0
  16. package/package.json +1 -1
  17. package/src/admin/components/category-tree/canvas-dock.tsx +71 -18
  18. package/src/admin/components/category-tree/category-node.tsx +48 -9
  19. package/src/admin/components/category-tree/category-products-manager.tsx +749 -0
  20. package/src/admin/components/category-tree/category-tree-canvas.tsx +59 -2
  21. package/src/admin/components/category-tree/custom-edge.tsx +6 -2
  22. package/src/admin/components/category-tree/edit-category-drawer.tsx +413 -118
  23. package/src/admin/components/category-tree/lib/layout-engine.ts +161 -25
  24. package/src/admin/components/category-tree/lib/tree-state.ts +112 -0
  25. package/src/admin/components/category-tree/tier-bars.tsx +12 -5
  26. package/src/admin/components/category-tree/types.ts +4 -0
@@ -11,6 +11,11 @@ export interface CanvasDockProps {
11
11
  onExpandAll: () => void;
12
12
  onCollapseAll: () => void;
13
13
  onFocusRoot: () => void;
14
+ selectedCount?: number;
15
+ onExpandSelected?: () => void;
16
+ onCollapseSelected?: () => void;
17
+ isBoxSelect?: boolean;
18
+ onToggleBoxSelect?: () => void;
14
19
  onZoomIn: () => void;
15
20
  onZoomOut: () => void;
16
21
  onFitView: () => void;
@@ -4,7 +4,9 @@ exports.CanvasDock = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ui_1 = require("@medusajs/ui");
6
6
  const icons_1 = require("@medusajs/icons");
7
- const CanvasDock = ({ salesChannels, selectedStorefront, onSelectStorefront, direction, onToggleDirection, searchQuery, onSearchChange, onExpandAll, onCollapseAll, onFocusRoot, onFitView, onOpenCreateRoot, dirtyCount, isSaving, onSave, onReset, }) => {
8
- return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2 items-center pointer-events-auto", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2.5 px-3.5 py-2 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-md border border-ui-border-base rounded-2xl shadow-xl transition-all", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1.5 border-r border-ui-border-base pr-2.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-[11px] font-semibold text-ui-fg-muted uppercase tracking-wider", children: "Store:" }), (0, jsx_runtime_1.jsxs)("select", { value: selectedStorefront, onChange: (e) => onSelectStorefront(e.target.value), className: "text-xs bg-ui-bg-subtle text-ui-fg-base border border-ui-border-base rounded-lg px-2 py-1 font-medium focus:outline-none cursor-pointer", children: [(0, jsx_runtime_1.jsx)("option", { value: "all", children: "All Storefronts" }), salesChannels.map((sc) => ((0, jsx_runtime_1.jsx)("option", { value: sc.id, children: sc.name }, sc.id)))] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center border-r border-ui-border-base pr-2.5", children: (0, jsx_runtime_1.jsx)("button", { type: "button", title: direction === "TB" ? "Switch to Mind Map (Left-to-Right)" : "Switch to Vertical Tree (Top-to-Bottom)", onClick: onToggleDirection, 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", children: (0, jsx_runtime_1.jsx)("span", { children: direction === "TB" ? "⬇ Vertical" : "➡ Mind Map" }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "relative w-40 sm:w-48", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: "Search categories\u2026", value: searchQuery, onChange: (e) => onSearchChange(e.target.value), className: "w-full text-xs pl-7 pr-2.5 py-1 rounded-lg bg-ui-bg-subtle border border-ui-border-base text-ui-fg-base focus:bg-ui-bg-base focus:border-ui-border-interactive focus:outline-none transition-all placeholder:text-ui-fg-muted" }), (0, jsx_runtime_1.jsx)(icons_1.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" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 border-l border-ui-border-base pl-2.5", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", title: "Expand All Categories", onClick: onExpandAll, 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", children: "Expand" }), (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Collapse All Subcategories", onClick: onCollapseAll, 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", children: "Collapse" }), (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Focus on Root Categories", onClick: onFocusRoot, 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", children: "Root" })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-1 border-l border-ui-border-base pl-2.5", children: (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Fit View (Center)", onClick: onFitView, className: "p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-md transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.ArrowsPointingOut, { className: "w-3.5 h-3.5" }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "border-l border-ui-border-base pl-2.5", children: (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "secondary", onClick: onOpenCreateRoot, className: "flex items-center gap-1 text-xs h-7", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "New Root" })] }) })] }), dirtyCount > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 px-4 py-1.5 bg-blue-600 text-white rounded-full shadow-lg text-xs font-medium animate-in fade-in slide-in-from-top-2 duration-200", children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-1.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "w-2 h-2 rounded-full bg-white animate-pulse" }), (0, jsx_runtime_1.jsxs)("span", { children: [dirtyCount, " categor", dirtyCount === 1 ? "y" : "ies", " repositioned"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 border-l border-blue-400 pl-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onReset, disabled: isSaving, className: "px-2 py-0.5 rounded hover:bg-blue-700 transition-colors text-blue-100 hover:text-white", children: "Reset" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onSave, disabled: isSaving, className: "px-3 py-0.5 rounded-full bg-white text-blue-700 font-semibold hover:bg-blue-50 transition-colors shadow-xs", children: isSaving ? "Saving…" : "Save Hierarchy" })] })] }))] }));
7
+ const CanvasDock = ({ salesChannels, selectedStorefront, onSelectStorefront, direction, onToggleDirection, searchQuery, onSearchChange, onExpandAll, onCollapseAll, onFocusRoot, selectedCount = 0, onExpandSelected, onCollapseSelected, isBoxSelect = false, onToggleBoxSelect, onFitView, onOpenCreateRoot, dirtyCount, isSaving, onSave, onReset, }) => {
8
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2 items-center pointer-events-auto", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2.5 px-3.5 py-2 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-md border border-ui-border-base rounded-2xl shadow-xl transition-all", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1.5 border-r border-ui-border-base pr-2.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-[11px] font-semibold text-ui-fg-muted uppercase tracking-wider", children: "Store:" }), (0, jsx_runtime_1.jsxs)("select", { value: selectedStorefront, onChange: (e) => onSelectStorefront(e.target.value), className: "text-xs bg-ui-bg-subtle text-ui-fg-base border border-ui-border-base rounded-lg px-2 py-1 font-medium focus:outline-none cursor-pointer", children: [(0, jsx_runtime_1.jsx)("option", { value: "all", children: "All Storefronts" }), salesChannels.map((sc) => ((0, jsx_runtime_1.jsx)("option", { value: sc.id, children: sc.name }, sc.id)))] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center border-r border-ui-border-base pr-2.5", children: (0, jsx_runtime_1.jsx)("button", { type: "button", title: direction === "TB" ? "Switch to Mind Map (Left-to-Right)" : "Switch to Vertical Tree (Top-to-Bottom)", onClick: onToggleDirection, 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", children: (0, jsx_runtime_1.jsx)("span", { children: direction === "TB" ? "⬇ Vertical" : "➡ Mind Map" }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "relative w-36 sm:w-44", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: "Search categories\u2026", value: searchQuery, onChange: (e) => onSearchChange(e.target.value), className: "w-full text-xs pl-7 pr-2.5 py-1 rounded-lg bg-ui-bg-subtle border border-ui-border-base text-ui-fg-base focus:bg-ui-bg-base focus:border-ui-border-interactive focus:outline-none transition-all placeholder:text-ui-fg-muted" }), (0, jsx_runtime_1.jsx)(icons_1.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" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 border-l border-ui-border-base pl-2.5", children: [selectedCount > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", title: `Expand selected category (${selectedCount} selected)`, onClick: onExpandSelected || onExpandAll, className: "px-2 py-1 text-xs font-medium bg-ui-bg-interactive text-ui-fg-on-color rounded-md transition-colors", children: ["Expand (", selectedCount, ")"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", title: `Collapse selected category (${selectedCount} selected)`, onClick: onCollapseSelected || onCollapseAll, 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", children: ["Collapse (", selectedCount, ")"] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", title: "Expand All Categories", onClick: onExpandAll, 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", children: "Expand All" }), (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Collapse All Subcategories", onClick: onCollapseAll, 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", children: "Collapse All" })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Focus on Root Categories", onClick: onFocusRoot, 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", children: "Root" })] }), onToggleBoxSelect && ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center border-l border-ui-border-base pl-2.5", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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)", onClick: onToggleBoxSelect, className: `px-2 py-1 text-xs font-medium rounded-md transition-colors ${isBoxSelect
9
+ ? "bg-blue-600 text-white shadow-xs"
10
+ : "text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle"}`, children: "\u26F6 Box Select" }) })), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-1 border-l border-ui-border-base pl-2.5", children: (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Fit View (Center)", onClick: onFitView, className: "p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-md transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.ArrowsPointingOut, { className: "w-3.5 h-3.5" }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "border-l border-ui-border-base pl-2.5", children: (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "secondary", onClick: onOpenCreateRoot, className: "flex items-center gap-1 text-xs h-7", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "New Root" })] }) })] }), dirtyCount > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 px-4 py-1.5 bg-blue-600 text-white rounded-full shadow-lg text-xs font-medium animate-in fade-in slide-in-from-top-2 duration-200", children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-1.5", children: [(0, jsx_runtime_1.jsx)("span", { className: "w-2 h-2 rounded-full bg-white animate-pulse" }), (0, jsx_runtime_1.jsxs)("span", { children: [dirtyCount, " categor", dirtyCount === 1 ? "y" : "ies", " repositioned"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 border-l border-blue-400 pl-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onReset, disabled: isSaving, className: "px-2 py-0.5 rounded hover:bg-blue-700 transition-colors text-blue-100 hover:text-white", children: "Reset" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onSave, disabled: isSaving, className: "px-3 py-0.5 rounded-full bg-white text-blue-700 font-semibold hover:bg-blue-50 transition-colors shadow-xs", children: isSaving ? "Saving…" : "Save Hierarchy" })] })] }))] }));
9
11
  };
10
12
  exports.CanvasDock = CanvasDock;
@@ -7,7 +7,7 @@ const react_2 = require("@xyflow/react");
7
7
  const icons_1 = require("@medusajs/icons");
8
8
  const tier_bars_1 = require("./tier-bars");
9
9
  exports.CategoryNode = (0, react_1.memo)(({ id, data, selected }) => {
10
- const { category, tier, superscriptRank, salesChannels, isCollapsed, hiddenDescendantCount, direction, isDimmed, onToggleCollapse, onAddChild, onEdit, onDetach, } = data;
10
+ const { category, tier, siblingRank, superscriptRank, branchColor, salesChannels, isCollapsed, hiddenDescendantCount, direction, isDimmed, onToggleCollapse, onAddChild, onEdit, onDetach, onMoveSiblingUp, onMoveSiblingDown, } = data;
11
11
  const isTB = direction === "TB";
12
12
  const hasChildren = category.has_children || category.children_count > 0;
13
13
  const assignedStorefrontNames = (category.sales_channels || []).map((scId) => {
@@ -16,7 +16,17 @@ exports.CategoryNode = (0, react_1.memo)(({ id, data, selected }) => {
16
16
  });
17
17
  return ((0, jsx_runtime_1.jsxs)("div", { className: `group relative flex items-stretch w-[270px] h-[86px] rounded-xl border bg-ui-bg-base transition-all duration-150 select-none ${selected
18
18
  ? "ring-2 ring-ui-border-interactive border-transparent shadow-lg"
19
- : "border-ui-border-base shadow-sm hover:shadow-md hover:border-ui-border-strong"} ${isDimmed ? "opacity-35 grayscale" : "opacity-100"}`, children: [(0, jsx_runtime_1.jsx)(react_2.Handle, { type: "target", position: isTB ? react_2.Position.Top : react_2.Position.Left, 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" }), (0, jsx_runtime_1.jsx)(tier_bars_1.TierBars, { tier: tier }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-1 items-center gap-2.5 px-2.5 py-2 min-w-0 overflow-hidden", children: [(0, jsx_runtime_1.jsxs)("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", children: [category.image_url ? ((0, jsx_runtime_1.jsx)("img", { src: category.image_url, alt: category.name, className: "w-full h-full object-cover", loading: "lazy" })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-full h-full flex items-center justify-center bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900 text-ui-fg-muted", children: (0, jsx_runtime_1.jsx)(icons_1.Folder, { className: "w-5 h-5 opacity-70" }) })), (0, jsx_runtime_1.jsx)("span", { className: `absolute bottom-0.5 right-0.5 w-2 h-2 rounded-full border border-white dark:border-neutral-900 ${category.is_active ? "bg-emerald-500" : "bg-neutral-400"}`, title: category.is_active ? "Active" : "Inactive" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col min-w-0 flex-1 justify-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-xs font-semibold text-ui-fg-base truncate leading-snug", title: category.name, children: category.name }), superscriptRank && ((0, jsx_runtime_1.jsx)("span", { className: "text-[11px] font-bold font-mono text-ui-fg-subtle px-1 py-0.5 rounded bg-ui-bg-subtle/80 flex-shrink-0", title: `Rank #${data.siblingRank} among siblings`, children: superscriptRank }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-[11px] text-ui-fg-muted truncate leading-tight mt-0.5", children: ["/", category.handle] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 mt-1 flex-wrap", children: [assignedStorefrontNames.slice(0, 2).map((name) => {
19
+ : "border-ui-border-base shadow-sm hover:shadow-md hover:border-ui-border-strong"} ${isDimmed ? "opacity-35 grayscale" : "opacity-100"}`, style: selected && branchColor
20
+ ? {
21
+ boxShadow: `0 0 0 2px ${branchColor}, 0 10px 15px -3px rgba(0, 0, 0, 0.1)`,
22
+ }
23
+ : undefined, children: [(0, jsx_runtime_1.jsx)(react_2.Handle, { type: "target", position: isTB ? react_2.Position.Top : react_2.Position.Left, 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" }), (0, jsx_runtime_1.jsx)(tier_bars_1.TierBars, { tier: tier, color: branchColor }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-1 items-center gap-2.5 px-2.5 py-2 min-w-0 overflow-hidden", children: [(0, jsx_runtime_1.jsxs)("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", children: [category.image_url ? ((0, jsx_runtime_1.jsx)("img", { src: category.image_url, alt: category.name, className: "w-full h-full object-cover", loading: "lazy" })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-full h-full flex items-center justify-center bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900 text-ui-fg-muted", children: (0, jsx_runtime_1.jsx)(icons_1.Folder, { className: "w-5 h-5 opacity-70" }) })), (0, jsx_runtime_1.jsx)("span", { className: `absolute bottom-0.5 right-0.5 w-2 h-2 rounded-full border border-white dark:border-neutral-900 ${category.is_active ? "bg-emerald-500" : "bg-neutral-400"}`, title: category.is_active ? "Active" : "Inactive" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col min-w-0 flex-1 justify-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 min-w-0", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-xs font-semibold text-ui-fg-base truncate leading-snug flex-1 min-w-0", title: category.name, children: category.name }), superscriptRank && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-0.5 flex-shrink-0", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-[11px] font-bold font-mono text-ui-fg-subtle px-1 py-0.5 rounded bg-ui-bg-subtle/80", title: `Rank #${siblingRank} among siblings (Storefront sort order)`, children: superscriptRank }), onMoveSiblingUp && ((0, jsx_runtime_1.jsx)("button", { type: "button", title: "Move Up in Storefront Order (\u2191)", onClick: (e) => {
24
+ e.stopPropagation();
25
+ onMoveSiblingUp(id);
26
+ }, 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", children: "\u2191" })), onMoveSiblingDown && ((0, jsx_runtime_1.jsx)("button", { type: "button", title: "Move Down in Storefront Order (\u2193)", onClick: (e) => {
27
+ e.stopPropagation();
28
+ onMoveSiblingDown(id);
29
+ }, 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", children: "\u2193" }))] }))] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-[11px] text-ui-fg-muted truncate leading-tight mt-0.5", children: ["/", category.handle] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 mt-1 flex-wrap", children: [assignedStorefrontNames.slice(0, 2).map((name) => {
20
30
  const isSuperpunch = name.toLowerCase().includes("superpunch");
21
31
  const isMonfil = name.toLowerCase().includes("monfil");
22
32
  const badgeText = isSuperpunch ? "SP" : isMonfil ? "M" : name.slice(0, 2).toUpperCase();
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ export interface CategoryProductsManagerProps {
3
+ categoryId: string;
4
+ categoryName: string;
5
+ initialMetadata?: Record<string, any> | null;
6
+ onMetadataUpdate?: (updatedMetadata: Record<string, any>) => void;
7
+ }
8
+ export declare const CategoryProductsManager: React.FC<CategoryProductsManagerProps>;
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CategoryProductsManager = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const ui_1 = require("@medusajs/ui");
7
+ const icons_1 = require("@medusajs/icons");
8
+ const sdk_1 = require("../../lib/sdk");
9
+ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, onMetadataUpdate, }) => {
10
+ const [products, setProducts] = (0, react_1.useState)([]);
11
+ const [totalCount, setTotalCount] = (0, react_1.useState)(0);
12
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
13
+ const [selectedIds, setSelectedIds] = (0, react_1.useState)(new Set());
14
+ const [isRemoving, setIsRemoving] = (0, react_1.useState)(false);
15
+ const [activeView, setActiveView] = (0, react_1.useState)("all");
16
+ // Product ranking and top sellers state
17
+ const [orderedProductIds, setOrderedProductIds] = (0, react_1.useState)([]);
18
+ const [topSellerIds, setTopSellerIds] = (0, react_1.useState)(new Set());
19
+ // Full-Screen Add Products FocusModal state (matching Medusa legacy modal)
20
+ const [focusModalOpen, setFocusModalOpen] = (0, react_1.useState)(false);
21
+ const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
22
+ const [catalogProducts, setCatalogProducts] = (0, react_1.useState)([]);
23
+ const [catalogTotalCount, setCatalogTotalCount] = (0, react_1.useState)(0);
24
+ const [catalogPage, setCatalogPage] = (0, react_1.useState)(0);
25
+ const [isSearchingCatalog, setIsSearchingCatalog] = (0, react_1.useState)(false);
26
+ const [catalogSelectedIds, setCatalogSelectedIds] = (0, react_1.useState)(new Set());
27
+ const [isAdding, setIsAdding] = (0, react_1.useState)(false);
28
+ const PAGE_SIZE = 50;
29
+ // Initialize ranking and top sellers from category metadata
30
+ (0, react_1.useEffect)(() => {
31
+ if (initialMetadata) {
32
+ if (Array.isArray(initialMetadata.ordered_product_ids)) {
33
+ setOrderedProductIds(initialMetadata.ordered_product_ids);
34
+ }
35
+ if (Array.isArray(initialMetadata.top_sellers)) {
36
+ setTopSellerIds(new Set(initialMetadata.top_sellers));
37
+ }
38
+ }
39
+ }, [initialMetadata]);
40
+ // Load products assigned to this category
41
+ const loadAssignedProducts = (0, react_1.useCallback)(async () => {
42
+ setIsLoading(true);
43
+ try {
44
+ const res = await sdk_1.sdk.client.fetch(`/admin/products?category_id[]=${categoryId}&limit=100&fields=id,title,thumbnail,status,variants.id,collection.title,sales_channels.name`);
45
+ const fetched = res.products || [];
46
+ setTotalCount(res.count ?? fetched.length);
47
+ // Sort products according to orderedProductIds if available
48
+ setProducts((prev) => {
49
+ const orderMap = new Map(orderedProductIds.map((id, index) => [id, index]));
50
+ return [...fetched].sort((a, b) => {
51
+ const indexA = orderMap.has(a.id) ? orderMap.get(a.id) : 9999;
52
+ const indexB = orderMap.has(b.id) ? orderMap.get(b.id) : 9999;
53
+ return indexA - indexB;
54
+ });
55
+ });
56
+ }
57
+ catch (err) {
58
+ ui_1.toast.error(err?.message || "Failed to load assigned products");
59
+ }
60
+ finally {
61
+ setIsLoading(false);
62
+ }
63
+ }, [categoryId, orderedProductIds]);
64
+ (0, react_1.useEffect)(() => {
65
+ loadAssignedProducts();
66
+ }, [loadAssignedProducts]);
67
+ // Save updated product rankings and top sellers to category metadata
68
+ const persistProductConfig = async (newOrder, newTopSellers) => {
69
+ const ranksMap = {};
70
+ newOrder.forEach((id, idx) => {
71
+ ranksMap[id] = idx + 1;
72
+ });
73
+ const updatedMetadata = {
74
+ ...(initialMetadata || {}),
75
+ ordered_product_ids: newOrder,
76
+ product_ranks: ranksMap,
77
+ top_sellers: Array.from(newTopSellers),
78
+ };
79
+ try {
80
+ await sdk_1.sdk.admin.productCategory.update(categoryId, {
81
+ metadata: updatedMetadata,
82
+ });
83
+ if (onMetadataUpdate) {
84
+ onMetadataUpdate(updatedMetadata);
85
+ }
86
+ }
87
+ catch (err) {
88
+ ui_1.toast.error(err?.message || "Failed to save product ordering");
89
+ }
90
+ };
91
+ // Sibling product ranking handlers (Move Up / Move Down)
92
+ const handleMoveProduct = async (index, direction) => {
93
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
94
+ if (targetIndex < 0 || targetIndex >= products.length)
95
+ return;
96
+ const reordered = [...products];
97
+ const [moved] = reordered.splice(index, 1);
98
+ reordered.splice(targetIndex, 0, moved);
99
+ setProducts(reordered);
100
+ const newOrder = reordered.map((p) => p.id);
101
+ setOrderedProductIds(newOrder);
102
+ ui_1.toast.info(`Moved "${moved.title}" to #${targetIndex + 1}`);
103
+ await persistProductConfig(newOrder, topSellerIds);
104
+ };
105
+ // Toggle Top Commercial Seller status
106
+ const handleToggleTopSeller = async (productId, productTitle) => {
107
+ const next = new Set(topSellerIds);
108
+ const isNowTop = !next.has(productId);
109
+ if (isNowTop) {
110
+ next.add(productId);
111
+ ui_1.toast.success(`Marked "${productTitle}" as Top Commercial Seller ⭐`);
112
+ }
113
+ else {
114
+ next.delete(productId);
115
+ ui_1.toast.info(`Removed "${productTitle}" from Top Commercial Sellers`);
116
+ }
117
+ setTopSellerIds(next);
118
+ const currentOrder = products.map((p) => p.id);
119
+ await persistProductConfig(currentOrder, next);
120
+ };
121
+ // Remove products from category
122
+ const handleRemoveProducts = async (idsToRemove) => {
123
+ if (idsToRemove.length === 0)
124
+ return;
125
+ const msg = idsToRemove.length === 1
126
+ ? "Are you sure you want to remove this product from the category?"
127
+ : `Are you sure you want to remove ${idsToRemove.length} products from this category?`;
128
+ if (!confirm(msg))
129
+ return;
130
+ setIsRemoving(true);
131
+ try {
132
+ await sdk_1.sdk.client.fetch(`/admin/product-categories/${categoryId}/products`, {
133
+ method: "POST",
134
+ body: { remove: idsToRemove },
135
+ });
136
+ ui_1.toast.success(`Removed ${idsToRemove.length} product(s) from category`);
137
+ setSelectedIds(new Set());
138
+ // Also clean up ranking and top sellers
139
+ const removeSet = new Set(idsToRemove);
140
+ const newOrder = orderedProductIds.filter((id) => !removeSet.has(id));
141
+ const newTopSellers = new Set(Array.from(topSellerIds).filter((id) => !removeSet.has(id)));
142
+ setOrderedProductIds(newOrder);
143
+ setTopSellerIds(newTopSellers);
144
+ await persistProductConfig(newOrder, newTopSellers);
145
+ await loadAssignedProducts();
146
+ }
147
+ catch (err) {
148
+ ui_1.toast.error(err?.message || "Failed to remove products");
149
+ }
150
+ finally {
151
+ setIsRemoving(false);
152
+ }
153
+ };
154
+ // Search catalog for the Add Products FocusModal (matching Medusa's legacy product table)
155
+ const searchCatalog = (0, react_1.useCallback)(async (query, page) => {
156
+ setIsSearchingCatalog(true);
157
+ try {
158
+ const qParam = query.trim() ? `&q=${encodeURIComponent(query.trim())}` : "";
159
+ const offset = page * PAGE_SIZE;
160
+ const res = await sdk_1.sdk.client.fetch(`/admin/products?limit=${PAGE_SIZE}&offset=${offset}${qParam}&fields=id,title,thumbnail,status,variants.id,collection.title,sales_channels.name`);
161
+ setCatalogProducts(res.products || []);
162
+ setCatalogTotalCount(res.count ?? (res.products?.length || 0));
163
+ }
164
+ catch (err) {
165
+ ui_1.toast.error(err?.message || "Failed to search catalog");
166
+ }
167
+ finally {
168
+ setIsSearchingCatalog(false);
169
+ }
170
+ }, []);
171
+ (0, react_1.useEffect)(() => {
172
+ if (focusModalOpen) {
173
+ searchCatalog(searchQuery, catalogPage);
174
+ }
175
+ }, [focusModalOpen, searchQuery, catalogPage, searchCatalog]);
176
+ const handleSaveAddedProducts = async () => {
177
+ const idsToAdd = Array.from(catalogSelectedIds);
178
+ if (idsToAdd.length === 0)
179
+ return;
180
+ setIsAdding(true);
181
+ try {
182
+ await sdk_1.sdk.client.fetch(`/admin/product-categories/${categoryId}/products`, {
183
+ method: "POST",
184
+ body: { add: idsToAdd },
185
+ });
186
+ ui_1.toast.success(`Added ${idsToAdd.length} product(s) to ${categoryName}`);
187
+ setFocusModalOpen(false);
188
+ setCatalogSelectedIds(new Set());
189
+ // Append new products to orderedProductIds
190
+ const newOrder = [...orderedProductIds, ...idsToAdd];
191
+ setOrderedProductIds(newOrder);
192
+ await persistProductConfig(newOrder, topSellerIds);
193
+ await loadAssignedProducts();
194
+ }
195
+ catch (err) {
196
+ ui_1.toast.error(err?.message || "Failed to add products");
197
+ }
198
+ finally {
199
+ setIsAdding(false);
200
+ }
201
+ };
202
+ const assignedProductIdsSet = (0, react_1.useMemo)(() => new Set(products.map((p) => p.id)), [products]);
203
+ const displayedProducts = (0, react_1.useMemo)(() => {
204
+ if (activeView === "top_sellers") {
205
+ return products.filter((p) => topSellerIds.has(p.id));
206
+ }
207
+ return products;
208
+ }, [products, activeView, topSellerIds]);
209
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1.5 p-1 rounded-lg bg-ui-bg-subtle border border-ui-border-base", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setActiveView("all"), className: `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${activeView === "all"
210
+ ? "bg-ui-bg-base text-ui-fg-base shadow-2xs font-semibold"
211
+ : "text-ui-fg-muted hover:text-ui-fg-base"}`, children: ["All Products (", products.length, ")"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setActiveView("top_sellers"), className: `flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${activeView === "top_sellers"
212
+ ? "bg-amber-500/10 text-amber-700 dark:text-amber-400 font-semibold"
213
+ : "text-ui-fg-muted hover:text-amber-600"}`, children: [(0, jsx_runtime_1.jsx)(icons_1.Sparkles, { className: "w-3.5 h-3.5 text-amber-500" }), (0, jsx_runtime_1.jsxs)("span", { children: ["Top Commercial Sellers (", topSellerIds.size, ")"] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [selectedIds.size > 0 && ((0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "danger", disabled: isRemoving, onClick: () => handleRemoveProducts(Array.from(selectedIds)), className: "flex items-center gap-1 text-xs", children: [(0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsxs)("span", { children: ["Remove Selected (", selectedIds.size, ")"] })] })), (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "secondary", onClick: () => {
214
+ setCatalogSelectedIds(new Set());
215
+ setSearchQuery("");
216
+ setCatalogPage(0);
217
+ setFocusModalOpen(true);
218
+ }, className: "flex items-center gap-1 text-xs", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Add Products" })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-base shadow-2xs", children: isLoading ? ((0, jsx_runtime_1.jsx)("div", { className: "p-8 text-center text-xs text-ui-fg-muted", children: "Loading products\u2026" })) : displayedProducts.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "p-8 text-center flex flex-col items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs text-ui-fg-muted", children: activeView === "top_sellers"
219
+ ? "No products marked as Top Commercial Sellers yet. Star a product below to highlight it."
220
+ : "No products assigned to this category." }), activeView === "all" && ((0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "secondary", onClick: () => setFocusModalOpen(true), className: "flex items-center gap-1 text-xs", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Add Products" })] }))] })) : ((0, jsx_runtime_1.jsxs)("table", { className: "w-full text-left text-xs border-collapse", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { className: "border-b border-ui-border-base bg-ui-bg-subtle/60 text-ui-fg-muted font-medium", children: [(0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3 w-8", children: (0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: displayedProducts.length > 0 && selectedIds.size === displayedProducts.length, onChange: () => {
221
+ if (selectedIds.size === displayedProducts.length)
222
+ setSelectedIds(new Set());
223
+ else
224
+ setSelectedIds(new Set(displayedProducts.map((p) => p.id)));
225
+ }, className: "rounded border-ui-border-base cursor-pointer" }) }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3 w-20", children: "Rank" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Product" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Top Seller" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Collection" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Variants" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Status" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3 text-right", children: "Action" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { className: "divide-y divide-ui-border-base", children: displayedProducts.map((p, index) => {
226
+ const isChecked = selectedIds.has(p.id);
227
+ const isTopSeller = topSellerIds.has(p.id);
228
+ return ((0, jsx_runtime_1.jsxs)("tr", { className: `hover:bg-ui-bg-subtle/40 transition-colors ${isChecked ? "bg-blue-50/40 dark:bg-blue-950/20" : ""}`, children: [(0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: isChecked, onChange: () => {
229
+ setSelectedIds((prev) => {
230
+ const next = new Set(prev);
231
+ if (next.has(p.id))
232
+ next.delete(p.id);
233
+ else
234
+ next.add(p.id);
235
+ return next;
236
+ });
237
+ }, className: "rounded border-ui-border-base cursor-pointer" }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 font-mono", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-xs font-bold text-ui-fg-subtle px-1.5 py-0.5 rounded bg-ui-bg-subtle border border-ui-border-base", children: ["#", index + 1] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", title: "Move Up", disabled: index === 0, onClick: () => handleMoveProduct(index, "up"), className: "text-[9px] text-ui-fg-muted hover:text-ui-fg-base disabled:opacity-20 px-0.5 leading-none", children: "\u25B2" }), (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Move Down", disabled: index === displayedProducts.length - 1, onClick: () => handleMoveProduct(index, "down"), className: "text-[9px] text-ui-fg-muted hover:text-ui-fg-base disabled:opacity-20 px-0.5 leading-none", children: "\u25BC" })] })] }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2.5 min-w-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "w-9 h-9 rounded-md border border-ui-border-base overflow-hidden bg-ui-bg-subtle flex-shrink-0 flex items-center justify-center", children: p.thumbnail ? ((0, jsx_runtime_1.jsx)("img", { src: p.thumbnail, alt: p.title, className: "w-full h-full object-cover" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-[9px] text-ui-fg-muted", children: "IMG" })) }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col min-w-0", children: (0, jsx_runtime_1.jsx)("span", { className: "font-semibold text-ui-fg-base truncate leading-snug", title: p.title, children: p.title }) })] }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => handleToggleTopSeller(p.id, p.title), className: `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border transition-all ${isTopSeller
238
+ ? "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-300 dark:border-amber-700 shadow-2xs"
239
+ : "bg-ui-bg-subtle text-ui-fg-muted border-ui-border-base hover:text-amber-600 hover:border-amber-300"}`, title: isTopSeller
240
+ ? "Click to remove from Top Commercial Sellers"
241
+ : "Click to mark as Top Commercial Seller (Reflected in Storefront 2)", children: [(0, jsx_runtime_1.jsx)("span", { children: isTopSeller ? "★" : "☆" }), (0, jsx_runtime_1.jsx)("span", { children: isTopSeller ? "Top Seller" : "Normal" })] }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3 text-ui-fg-muted", children: p.collection?.title || "-" }), (0, jsx_runtime_1.jsxs)("td", { className: "py-2.5 px-3 text-ui-fg-subtle", children: [p.variants?.length || 0, " variant(s)"] }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsx)("span", { className: `inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium ${p.status === "published"
242
+ ? "bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300"
243
+ : "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"}`, children: p.status }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3 text-right", children: (0, jsx_runtime_1.jsx)("button", { type: "button", title: "Remove from category", onClick: () => handleRemoveProducts([p.id]), className: "p-1 text-ui-fg-muted hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3.5 h-3.5" }) }) })] }, p.id));
244
+ }) })] })) }), focusModalOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "fixed inset-0 z-50 flex flex-col bg-white dark:bg-neutral-950 animate-in fade-in-50", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between px-6 py-3 border-b border-ui-border-base bg-ui-bg-base flex-shrink-0", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setFocusModalOpen(false), className: "p-1.5 rounded-lg text-ui-fg-muted hover:text-ui-fg-base hover:bg-ui-bg-subtle transition-colors", title: "Close (ESC)", children: (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-bold", children: "\u2715" }) }), (0, jsx_runtime_1.jsx)("span", { className: "text-[11px] font-mono font-medium text-ui-fg-muted px-1.5 py-0.5 rounded bg-ui-bg-subtle border border-ui-border-base", children: "ESC" }), (0, jsx_runtime_1.jsxs)("h2", { className: "text-sm font-semibold text-ui-fg-base", children: ["Add Products to Category: ", categoryName] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "relative w-64", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: "Search products\u2026", value: searchQuery, onChange: (e) => {
245
+ setSearchQuery(e.target.value);
246
+ setCatalogPage(0);
247
+ }, className: "w-full text-xs pl-8 pr-3 py-1.5 rounded-lg bg-ui-bg-subtle border border-ui-border-base text-ui-fg-base focus:bg-ui-bg-base focus:border-ui-border-interactive focus:outline-none placeholder:text-ui-fg-muted", autoFocus: true }), (0, jsx_runtime_1.jsx)(icons_1.MagnifyingGlass, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-ui-fg-muted pointer-events-none" })] }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex-1 overflow-y-auto px-6 py-4", children: (0, jsx_runtime_1.jsx)("div", { className: "border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-base shadow-sm", children: (0, jsx_runtime_1.jsxs)("table", { className: "w-full text-left text-xs border-collapse", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { className: "border-b border-ui-border-base bg-ui-bg-subtle/80 text-ui-fg-muted font-medium", children: [(0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3 w-8", children: (0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: catalogProducts.length > 0 &&
248
+ catalogProducts.every((p) => assignedProductIdsSet.has(p.id) || catalogSelectedIds.has(p.id)), onChange: () => {
249
+ const available = catalogProducts.filter((p) => !assignedProductIdsSet.has(p.id));
250
+ if (available.every((p) => catalogSelectedIds.has(p.id))) {
251
+ setCatalogSelectedIds(new Set());
252
+ }
253
+ else {
254
+ setCatalogSelectedIds(new Set(available.map((p) => p.id)));
255
+ }
256
+ }, className: "rounded border-ui-border-base cursor-pointer" }) }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Product" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Collection" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Sales Channels" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Variants" }), (0, jsx_runtime_1.jsx)("th", { className: "py-2.5 px-3", children: "Status" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { className: "divide-y divide-ui-border-base", children: isSearchingCatalog ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: 6, className: "py-12 text-center text-ui-fg-muted", children: "Searching catalog products\u2026" }) })) : catalogProducts.length === 0 ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: 6, className: "py-12 text-center text-ui-fg-muted", children: "No products found." }) })) : (catalogProducts.map((p) => {
257
+ const isAlreadyAssigned = assignedProductIdsSet.has(p.id);
258
+ const isSelected = catalogSelectedIds.has(p.id) || isAlreadyAssigned;
259
+ return ((0, jsx_runtime_1.jsxs)("tr", { onClick: () => {
260
+ if (isAlreadyAssigned)
261
+ return;
262
+ setCatalogSelectedIds((prev) => {
263
+ const next = new Set(prev);
264
+ if (next.has(p.id))
265
+ next.delete(p.id);
266
+ else
267
+ next.add(p.id);
268
+ return next;
269
+ });
270
+ }, className: `cursor-pointer transition-colors ${isAlreadyAssigned
271
+ ? "opacity-50 bg-ui-bg-subtle/30 cursor-not-allowed"
272
+ : isSelected
273
+ ? "bg-blue-50/50 dark:bg-blue-950/30"
274
+ : "hover:bg-ui-bg-subtle/50"}`, children: [(0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", onClick: (e) => e.stopPropagation(), children: (0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: isSelected, disabled: isAlreadyAssigned, onChange: () => {
275
+ if (isAlreadyAssigned)
276
+ return;
277
+ setCatalogSelectedIds((prev) => {
278
+ const next = new Set(prev);
279
+ if (next.has(p.id))
280
+ next.delete(p.id);
281
+ else
282
+ next.add(p.id);
283
+ return next;
284
+ });
285
+ }, className: "rounded border-ui-border-base cursor-pointer" }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2.5 min-w-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "w-8 h-8 rounded border border-ui-border-base overflow-hidden bg-ui-bg-subtle flex-shrink-0 flex items-center justify-center", children: p.thumbnail ? ((0, jsx_runtime_1.jsx)("img", { src: p.thumbnail, alt: p.title, className: "w-full h-full object-cover" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-[8px] text-ui-fg-muted", children: "IMG" })) }), (0, jsx_runtime_1.jsx)("span", { className: "font-semibold text-ui-fg-base truncate leading-snug", children: p.title }), isAlreadyAssigned && ((0, jsx_runtime_1.jsx)(ui_1.Badge, { size: "small", color: "grey", className: "ml-1 text-[9px]", children: "Already in Category" }))] }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3 text-ui-fg-muted", children: p.collection?.title || "-" }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-1 flex-wrap", children: (p.sales_channels || []).map((sc) => ((0, jsx_runtime_1.jsx)("span", { className: "px-1.5 py-0.5 rounded text-[10px] bg-ui-bg-subtle border border-ui-border-base text-ui-fg-subtle", children: sc.name }, sc.id))) }) }), (0, jsx_runtime_1.jsxs)("td", { className: "py-2.5 px-3 text-ui-fg-subtle", children: [p.variants?.length || 0, " variant(s)"] }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsx)("span", { className: `inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium ${p.status === "published"
286
+ ? "bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300"
287
+ : "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"}`, children: p.status }) })] }, p.id));
288
+ })) })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between px-6 py-3 border-t border-ui-border-base bg-ui-bg-base flex-shrink-0", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-xs text-ui-fg-muted", children: ["Showing ", Math.min(catalogProducts.length, PAGE_SIZE), " of ", catalogTotalCount, " results"] }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs text-ui-fg-muted", children: "\u2022" }), (0, jsx_runtime_1.jsxs)("span", { className: "text-xs font-semibold text-ui-fg-base", children: [catalogSelectedIds.size, " product(s) selected"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", disabled: catalogPage === 0, onClick: () => setCatalogPage((p) => Math.max(0, p - 1)), className: "px-2.5 py-1 text-xs rounded border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base disabled:opacity-40", children: "Prev" }), (0, jsx_runtime_1.jsx)("button", { type: "button", disabled: (catalogPage + 1) * PAGE_SIZE >= catalogTotalCount, onClick: () => setCatalogPage((p) => p + 1), className: "px-2.5 py-1 text-xs rounded border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base disabled:opacity-40", children: "Next" }), (0, jsx_runtime_1.jsx)("div", { className: "h-4 w-[1px] bg-ui-border-base mx-1" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { size: "small", variant: "secondary", onClick: () => setFocusModalOpen(false), children: "Cancel" }), (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", variant: "primary", disabled: catalogSelectedIds.size === 0 || isAdding, onClick: handleSaveAddedProducts, className: "flex items-center gap-1 text-xs min-w-[70px] justify-center", children: [(0, jsx_runtime_1.jsx)(icons_1.CheckMini, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: isAdding ? "Saving…" : "Save" })] })] })] })] }))] }));
289
+ };
290
+ exports.CategoryProductsManager = CategoryProductsManager;
@@ -31,6 +31,8 @@ const CanvasInner = () => {
31
31
  const [selectedStorefront, setSelectedStorefront] = (0, react_1.useState)("all");
32
32
  const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
33
33
  const [collapsedIds, setCollapsedIds] = (0, react_1.useState)(new Set());
34
+ const [isBoxSelect, setIsBoxSelect] = (0, react_1.useState)(false);
35
+ const [selectedNodeIds, setSelectedNodeIds] = (0, react_1.useState)([]);
34
36
  const [createDrawerOpen, setCreateDrawerOpen] = (0, react_1.useState)(false);
35
37
  const [createParentCategory, setCreateParentCategory] = (0, react_1.useState)(null);
36
38
  const [editDrawerOpen, setEditDrawerOpen] = (0, react_1.useState)(false);
@@ -66,6 +68,7 @@ const CanvasInner = () => {
66
68
  }
67
69
  const tiers = (0, tree_state_1.computeTiers)(categories);
68
70
  const siblingRanks = (0, tree_state_1.computeSiblingRanks)(categories);
71
+ const branchColors = (0, tree_state_1.computeBranchColors)(categories);
69
72
  const enriched = categories.map((cat) => {
70
73
  const kids = map.get(cat.id) || [];
71
74
  const rankInfo = siblingRanks.get(cat.id);
@@ -75,6 +78,7 @@ const CanvasInner = () => {
75
78
  sibling_rank: rankInfo?.rankNum || 1,
76
79
  children_count: kids.length,
77
80
  has_children: kids.length > 0,
81
+ branch_color: branchColors.get(cat.id),
78
82
  };
79
83
  });
80
84
  return { childrenMap: map, enrichedCategories: enriched };
@@ -91,6 +95,30 @@ const CanvasInner = () => {
91
95
  return next;
92
96
  });
93
97
  }, []);
98
+ const handleExpandSelected = (0, react_1.useCallback)(() => {
99
+ if (selectedNodeIds.length === 0)
100
+ return;
101
+ setCollapsedIds((prev) => {
102
+ const next = new Set(prev);
103
+ selectedNodeIds.forEach((id) => next.delete(id));
104
+ return next;
105
+ });
106
+ }, [selectedNodeIds]);
107
+ const handleCollapseSelected = (0, react_1.useCallback)(() => {
108
+ if (selectedNodeIds.length === 0)
109
+ return;
110
+ setCollapsedIds((prev) => {
111
+ const next = new Set(prev);
112
+ selectedNodeIds.forEach((id) => next.add(id));
113
+ return next;
114
+ });
115
+ }, [selectedNodeIds]);
116
+ const handleMoveSiblingUp = (0, react_1.useCallback)((id) => {
117
+ setCategories((prev) => (0, tree_state_1.moveSiblingRank)(prev, id, "up"));
118
+ }, []);
119
+ const handleMoveSiblingDown = (0, react_1.useCallback)((id) => {
120
+ setCategories((prev) => (0, tree_state_1.moveSiblingRank)(prev, id, "down"));
121
+ }, []);
94
122
  const handleAddChild = (0, react_1.useCallback)((parent) => {
95
123
  setCreateParentCategory(parent);
96
124
  setCreateDrawerOpen(true);
@@ -127,6 +155,7 @@ const CanvasInner = () => {
127
155
  tier: cat.tier,
128
156
  siblingRank: rankInfo?.rankNum || 1,
129
157
  superscriptRank: rankInfo?.superscript || "¹",
158
+ branchColor: cat.branch_color,
130
159
  salesChannels,
131
160
  isCollapsed: collapsedIds.has(cat.id),
132
161
  hiddenDescendantCount: cat.children_count,
@@ -136,6 +165,8 @@ const CanvasInner = () => {
136
165
  onAddChild: handleAddChild,
137
166
  onEdit: handleEdit,
138
167
  onDetach: handleDetach,
168
+ onMoveSiblingUp: handleMoveSiblingUp,
169
+ onMoveSiblingDown: handleMoveSiblingDown,
139
170
  },
140
171
  };
141
172
  });
@@ -147,11 +178,18 @@ const CanvasInner = () => {
147
178
  source: cat.parent_category_id,
148
179
  target: cat.id,
149
180
  type: "smoothstep",
181
+ data: {
182
+ branchColor: cat.branch_color,
183
+ },
184
+ style: {
185
+ stroke: cat.branch_color || "#64748b",
186
+ strokeWidth: 2,
187
+ },
150
188
  });
151
189
  }
152
190
  }
153
191
  const { visibleNodes, visibleEdges } = (0, tree_state_1.getVisibleTree)(rawNodes, rawEdges, collapsedIds, childrenMap);
154
- const { nodes: layoutedNodes, edges: layoutedEdges } = (0, layout_engine_1.computeDagreLayout)(visibleNodes, visibleEdges, { direction });
192
+ const { nodes: layoutedNodes, edges: layoutedEdges } = (0, layout_engine_1.computeTreeLayout)(visibleNodes, visibleEdges, { direction });
155
193
  setNodes(layoutedNodes);
156
194
  setEdges(layoutedEdges);
157
195
  }, [
@@ -166,6 +204,8 @@ const CanvasInner = () => {
166
204
  handleAddChild,
167
205
  handleEdit,
168
206
  handleDetach,
207
+ handleMoveSiblingUp,
208
+ handleMoveSiblingDown,
169
209
  setNodes,
170
210
  setEdges,
171
211
  ]);
@@ -223,10 +263,12 @@ const CanvasInner = () => {
223
263
  setCategories([...originalCategories]);
224
264
  ui_1.toast.info("Changes reset to previous saved state");
225
265
  };
226
- return ((0, jsx_runtime_1.jsxs)("div", { className: "relative w-full h-[calc(100vh-56px)] overflow-hidden bg-[#fafafa] dark:bg-[#09090b]", children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 z-30 pointer-events-none", children: (0, jsx_runtime_1.jsx)(canvas_dock_1.CanvasDock, { salesChannels: salesChannels, selectedStorefront: selectedStorefront, onSelectStorefront: setSelectedStorefront, direction: direction, onToggleDirection: () => setDirection((d) => (d === "TB" ? "LR" : "TB")), searchQuery: searchQuery, onSearchChange: setSearchQuery, onExpandAll: handleExpandAll, onCollapseAll: handleCollapseAll, onFocusRoot: handleFocusRoot, onZoomIn: () => zoomIn({ duration: 300 }), onZoomOut: () => zoomOut({ duration: 300 }), onFitView: () => fitView({ padding: 0.25, duration: 400 }), onOpenCreateRoot: () => {
266
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "relative w-full h-[calc(100vh-56px)] overflow-hidden bg-[#fafafa] dark:bg-[#09090b]", children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 z-30 pointer-events-none", children: (0, jsx_runtime_1.jsx)(canvas_dock_1.CanvasDock, { salesChannels: salesChannels, selectedStorefront: selectedStorefront, onSelectStorefront: setSelectedStorefront, direction: direction, onToggleDirection: () => setDirection((d) => (d === "TB" ? "LR" : "TB")), searchQuery: searchQuery, onSearchChange: setSearchQuery, onExpandAll: handleExpandAll, onCollapseAll: handleCollapseAll, onFocusRoot: handleFocusRoot, selectedCount: selectedNodeIds.length, onExpandSelected: handleExpandSelected, onCollapseSelected: handleCollapseSelected, isBoxSelect: isBoxSelect, onToggleBoxSelect: () => setIsBoxSelect((prev) => !prev), onZoomIn: () => zoomIn({ duration: 300 }), onZoomOut: () => zoomOut({ duration: 300 }), onFitView: () => fitView({ padding: 0.25, duration: 400 }), onOpenCreateRoot: () => {
227
267
  setCreateParentCategory(null);
228
268
  setCreateDrawerOpen(true);
229
- }, dirtyCount: dirtyMoves.length, isSaving: isSaving, onSave: handleSave, onReset: handleReset }) }), (0, jsx_runtime_1.jsxs)(react_2.ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: onConnect, onEdgesDelete: onEdgesDelete, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.2, maxZoom: 2.0, defaultEdgeOptions: { type: "smoothstep" }, fitView: true, proOptions: { hideAttribution: true }, className: "touch-none", children: [(0, jsx_runtime_1.jsx)(react_2.Background, { color: "#cbd5e1", gap: 20, size: 1.25 }), (0, jsx_runtime_1.jsx)(react_2.Controls, { showInteractive: false, className: "!bottom-4 !left-4 !bg-white/90 dark:!bg-neutral-900/90 !backdrop-blur-md !border !border-ui-border-base !rounded-xl !shadow-md !overflow-hidden" }), (0, jsx_runtime_1.jsx)(react_2.MiniMap, { nodeColor: () => "var(--bg-interactive, #3b82f6)", maskColor: "rgba(0, 0, 0, 0.08)", className: "!bottom-4 !right-4 !bg-white/90 dark:!bg-neutral-900/90 !backdrop-blur-md !border !border-ui-border-base !rounded-xl !shadow-md !overflow-hidden", zoomable: true, pannable: true })] }), (0, jsx_runtime_1.jsx)(create_category_drawer_1.CreateCategoryDrawer, { open: createDrawerOpen, onOpenChange: setCreateDrawerOpen, parentCategory: createParentCategory, salesChannels: salesChannels, onSuccess: (newCat) => {
269
+ }, dirtyCount: dirtyMoves.length, isSaving: isSaving, onSave: handleSave, onReset: handleReset }) }), (0, jsx_runtime_1.jsxs)(react_2.ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: onConnect, onEdgesDelete: onEdgesDelete, onSelectionChange: (0, react_1.useCallback)(({ nodes }) => {
270
+ setSelectedNodeIds(nodes.map((n) => n.id));
271
+ }, []), selectionMode: react_2.SelectionMode.Partial, selectionOnDrag: isBoxSelect, panOnDrag: isBoxSelect ? [1, 2] : true, elementsSelectable: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.2, maxZoom: 2.0, defaultEdgeOptions: { type: "smoothstep" }, fitView: true, proOptions: { hideAttribution: true }, className: "touch-none", children: [(0, jsx_runtime_1.jsx)(react_2.Background, { color: "#cbd5e1", gap: 20, size: 1.25 }), (0, jsx_runtime_1.jsx)(react_2.Controls, { showInteractive: false, className: "!bottom-4 !left-4 !bg-white/90 dark:!bg-neutral-900/90 !backdrop-blur-md !border !border-ui-border-base !rounded-xl !shadow-md !overflow-hidden" }), (0, jsx_runtime_1.jsx)(react_2.MiniMap, { nodeColor: () => "var(--bg-interactive, #3b82f6)", maskColor: "rgba(0, 0, 0, 0.08)", className: "!bottom-4 !right-4 !bg-white/90 dark:!bg-neutral-900/90 !backdrop-blur-md !border !border-ui-border-base !rounded-xl !shadow-md !overflow-hidden", zoomable: true, pannable: true })] }), (0, jsx_runtime_1.jsx)(create_category_drawer_1.CreateCategoryDrawer, { open: createDrawerOpen, onOpenChange: setCreateDrawerOpen, parentCategory: createParentCategory, salesChannels: salesChannels, onSuccess: (newCat) => {
230
272
  setCategories((prev) => [...prev, newCat]);
231
273
  setOriginalCategories((prev) => [...prev, newCat]);
232
274
  } }), (0, jsx_runtime_1.jsx)(edit_category_drawer_1.EditCategoryDrawer, { open: editDrawerOpen, onOpenChange: setEditDrawerOpen, category: editingCategory, salesChannels: salesChannels, onSuccess: (updatedCat) => {
@@ -5,7 +5,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const react_2 = require("@xyflow/react");
7
7
  exports.CustomSmoothStepEdge = (0, react_1.memo)((props) => {
8
- const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerEnd, selected, } = props;
8
+ const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, data, markerEnd, selected, } = props;
9
+ const branchColor = data?.branchColor || style?.stroke || "#64748b";
9
10
  const [edgePath] = (0, react_2.getSmoothStepPath)({
10
11
  sourceX,
11
12
  sourceY,
@@ -17,8 +18,9 @@ exports.CustomSmoothStepEdge = (0, react_1.memo)((props) => {
17
18
  });
18
19
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("path", { d: edgePath, fill: "none", strokeOpacity: 0, strokeWidth: 20, className: "react-flow__edge-interaction cursor-pointer" }), (0, jsx_runtime_1.jsx)(react_2.BaseEdge, { id: id, path: edgePath, markerEnd: markerEnd, style: {
19
20
  ...style,
20
- stroke: selected ? "var(--bg-interactive, #3b82f6)" : "var(--border-strong, #cbd5e1)",
21
- strokeWidth: selected ? 2.5 : 1.75,
21
+ stroke: selected ? "#2563eb" : branchColor,
22
+ strokeWidth: selected ? 2.75 : 2,
23
+ strokeOpacity: 0.95,
22
24
  transition: "stroke 0.2s, stroke-width 0.2s",
23
25
  } })] }));
24
26
  });