@hectasquare/medusa-plugin-category-tree 0.0.0-stage → 1.0.0
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 +23 -0
- package/.medusa/server/src/admin/components/category-tree/canvas-dock.js +10 -0
- package/.medusa/server/src/admin/components/category-tree/category-node.d.ts +9 -0
- package/.medusa/server/src/admin/components/category-tree/category-node.js +41 -0
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.d.ts +3 -0
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +243 -0
- package/.medusa/server/src/admin/components/category-tree/create-category-drawer.d.ts +10 -0
- package/.medusa/server/src/admin/components/category-tree/create-category-drawer.js +89 -0
- package/.medusa/server/src/admin/components/category-tree/custom-edge.d.ts +3 -0
- package/.medusa/server/src/admin/components/category-tree/custom-edge.js +25 -0
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.d.ts +11 -0
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +109 -0
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.d.ts +12 -0
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.js +44 -0
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.d.ts +15 -0
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.js +129 -0
- package/.medusa/server/src/admin/components/category-tree/tier-bars.d.ts +13 -0
- package/.medusa/server/src/admin/components/category-tree/tier-bars.js +17 -0
- package/.medusa/server/src/admin/components/category-tree/types.d.ts +47 -0
- package/.medusa/server/src/admin/components/category-tree/types.js +2 -0
- package/.medusa/server/src/admin/index.d.ts +28 -0
- package/.medusa/server/src/admin/index.js +66 -0
- package/.medusa/server/src/admin/lib/sdk.d.ts +2 -0
- package/.medusa/server/src/admin/lib/sdk.js +19 -0
- package/.medusa/server/src/admin/routes/category-tree/page.d.ts +3 -0
- package/.medusa/server/src/admin/routes/category-tree/page.js +15 -0
- package/.medusa/server/src/api/admin/category-tree/create/route.d.ts +2 -0
- package/.medusa/server/src/api/admin/category-tree/create/route.js +44 -0
- package/.medusa/server/src/api/admin/category-tree/reorder/route.d.ts +2 -0
- package/.medusa/server/src/api/admin/category-tree/reorder/route.js +76 -0
- package/.medusa/server/src/api/admin/category-tree/route.d.ts +2 -0
- package/.medusa/server/src/api/admin/category-tree/route.js +98 -0
- package/.medusa/server/src/api/admin/category-tree/validators.d.ts +74 -0
- package/.medusa/server/src/api/admin/category-tree/validators.js +23 -0
- package/.medusa/server/src/api/middlewares.d.ts +2 -0
- package/.medusa/server/src/api/middlewares.js +13 -0
- package/.medusa/server/src/index.d.ts +1 -0
- package/.medusa/server/src/index.js +17 -0
- package/.medusa/server/src/workflows/index.d.ts +1 -0
- package/.medusa/server/src/workflows/index.js +17 -0
- package/.medusa/server/src/workflows/reorder-categories.d.ts +10 -0
- package/.medusa/server/src/workflows/reorder-categories.js +92 -0
- package/README.md +228 -2
- package/admin/index.js +5 -0
- package/admin/index.mjs +3 -0
- package/package.json +75 -4
- package/src/admin/components/category-tree/canvas-dock.tsx +186 -0
- package/src/admin/components/category-tree/category-node.tsx +188 -0
- package/src/admin/components/category-tree/category-tree-canvas.tsx +397 -0
- package/src/admin/components/category-tree/create-category-drawer.tsx +253 -0
- package/src/admin/components/category-tree/custom-edge.tsx +53 -0
- package/src/admin/components/category-tree/edit-category-drawer.tsx +279 -0
- package/src/admin/components/category-tree/lib/layout-engine.ts +56 -0
- package/src/admin/components/category-tree/lib/tree-state.ts +167 -0
- package/src/admin/components/category-tree/tier-bars.tsx +31 -0
- package/src/admin/components/category-tree/types.ts +51 -0
- package/src/admin/index.tsx +36 -0
- package/src/admin/lib/sdk.ts +15 -0
- package/src/admin/routes/category-tree/page.tsx +14 -0
- package/src/api/admin/category-tree/create/route.ts +68 -0
- package/src/api/admin/category-tree/reorder/route.ts +111 -0
- package/src/api/admin/category-tree/route.ts +115 -0
- package/src/api/admin/category-tree/validators.ts +27 -0
- package/src/api/middlewares.ts +15 -0
- package/src/index.ts +1 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/reorder-categories.ts +149 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SalesChannelInfo } from "./types";
|
|
3
|
+
export interface CanvasDockProps {
|
|
4
|
+
salesChannels: SalesChannelInfo[];
|
|
5
|
+
selectedStorefront: string;
|
|
6
|
+
onSelectStorefront: (val: string) => void;
|
|
7
|
+
direction: "TB" | "LR";
|
|
8
|
+
onToggleDirection: () => void;
|
|
9
|
+
searchQuery: string;
|
|
10
|
+
onSearchChange: (q: string) => void;
|
|
11
|
+
onExpandAll: () => void;
|
|
12
|
+
onCollapseAll: () => void;
|
|
13
|
+
onFocusRoot: () => void;
|
|
14
|
+
onZoomIn: () => void;
|
|
15
|
+
onZoomOut: () => void;
|
|
16
|
+
onFitView: () => void;
|
|
17
|
+
onOpenCreateRoot: () => void;
|
|
18
|
+
dirtyCount: number;
|
|
19
|
+
isSaving: boolean;
|
|
20
|
+
onSave: () => void;
|
|
21
|
+
onReset: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare const CanvasDock: React.FC<CanvasDockProps>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CanvasDock = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const ui_1 = require("@medusajs/ui");
|
|
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)(ui_1.Tooltip, { content: direction === "TB" ? "Switch to Mind Map (Left-to-Right)" : "Switch to Vertical Tree (Top-to-Bottom)", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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)(ui_1.Tooltip, { content: "Expand All Categories", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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)(ui_1.Tooltip, { content: "Collapse All Subcategories", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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)(ui_1.Tooltip, { content: "Focus on Root Categories", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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)(ui_1.Tooltip, { content: "Fit View (Center)", children: (0, jsx_runtime_1.jsx)("button", { type: "button", 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
|
+
};
|
|
10
|
+
exports.CanvasDock = CanvasDock;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CategoryNodeData } from "./types";
|
|
3
|
+
interface CategoryNodeProps {
|
|
4
|
+
id: string;
|
|
5
|
+
data: CategoryNodeData;
|
|
6
|
+
selected?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const CategoryNode: React.MemoExoticComponent<({ id, data, selected }: CategoryNodeProps) => import("react/jsx-runtime").JSX.Element>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CategoryNode = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_2 = require("@xyflow/react");
|
|
7
|
+
const ui_1 = require("@medusajs/ui");
|
|
8
|
+
const icons_1 = require("@medusajs/icons");
|
|
9
|
+
const tier_bars_1 = require("./tier-bars");
|
|
10
|
+
exports.CategoryNode = (0, react_1.memo)(({ id, data, selected }) => {
|
|
11
|
+
const { category, tier, superscriptRank, salesChannels, isCollapsed, hiddenDescendantCount, direction, isDimmed, onToggleCollapse, onAddChild, onEdit, onDetach, } = data;
|
|
12
|
+
const isTB = direction === "TB";
|
|
13
|
+
const hasChildren = category.has_children || category.children_count > 0;
|
|
14
|
+
const assignedStorefrontNames = (category.sales_channels || []).map((scId) => {
|
|
15
|
+
const sc = salesChannels.find((s) => s.id === scId);
|
|
16
|
+
return sc?.name || scId;
|
|
17
|
+
});
|
|
18
|
+
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
|
|
19
|
+
? "ring-2 ring-ui-border-interactive border-transparent shadow-lg"
|
|
20
|
+
: "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) => {
|
|
21
|
+
const isSuperpunch = name.toLowerCase().includes("superpunch");
|
|
22
|
+
const isMonfil = name.toLowerCase().includes("monfil");
|
|
23
|
+
const badgeText = isSuperpunch ? "SP" : isMonfil ? "M" : name.slice(0, 2).toUpperCase();
|
|
24
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: "px-1 py-[1px] text-[9px] font-semibold rounded bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 border border-neutral-200/60 dark:border-neutral-700/60", title: `Storefront: ${name}`, children: badgeText }, name));
|
|
25
|
+
}), hasChildren && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: (e) => {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
onToggleCollapse(id);
|
|
28
|
+
}, className: `ml-auto px-1.5 py-0.5 rounded text-[10px] font-medium flex items-center gap-0.5 transition-colors ${isCollapsed
|
|
29
|
+
? "bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-950/60 dark:text-blue-300 dark:border-blue-800"
|
|
30
|
+
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-300"}`, title: isCollapsed ? "Click to expand subcategories" : "Click to collapse subcategories", children: [(0, jsx_runtime_1.jsx)("span", { children: isCollapsed ? "▸" : "▾" }), (0, jsx_runtime_1.jsx)("span", { children: isCollapsed ? `+${hiddenDescendantCount || category.children_count}` : category.children_count || "sub" })] }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute -top-3 right-2 hidden group-hover:flex items-center gap-1 px-1.5 py-0.5 bg-ui-bg-base/95 backdrop-blur-sm border border-ui-border-base rounded-full shadow-md z-20", children: [(0, jsx_runtime_1.jsx)(ui_1.Tooltip, { content: "Add Subcategory", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
onAddChild(category);
|
|
33
|
+
}, className: "p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-full transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }) }) }), (0, jsx_runtime_1.jsx)(ui_1.Tooltip, { content: "Edit Category", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
onEdit(category);
|
|
36
|
+
}, className: "p-1 text-ui-fg-subtle hover:text-ui-fg-base hover:bg-ui-bg-subtle rounded-full transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.PencilSquare, { className: "w-3.5 h-3.5" }) }) }), category.parent_category_id && ((0, jsx_runtime_1.jsx)(ui_1.Tooltip, { content: "Promote to Root (Detach)", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
onDetach(id);
|
|
39
|
+
}, className: "p-1 text-ui-fg-subtle hover:text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-950/50 rounded-full transition-colors", children: (0, jsx_runtime_1.jsx)(icons_1.ArrowUturnLeft, { className: "w-3.5 h-3.5" }) }) }))] }), (0, jsx_runtime_1.jsx)(react_2.Handle, { type: "source", position: isTB ? react_2.Position.Bottom : react_2.Position.Right, 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" })] }));
|
|
40
|
+
});
|
|
41
|
+
exports.CategoryNode.displayName = "CategoryNode";
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CategoryTreeCanvas = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_2 = require("@xyflow/react");
|
|
7
|
+
require("@xyflow/react/dist/style.css");
|
|
8
|
+
const ui_1 = require("@medusajs/ui");
|
|
9
|
+
const category_node_1 = require("./category-node");
|
|
10
|
+
const custom_edge_1 = require("./custom-edge");
|
|
11
|
+
const canvas_dock_1 = require("./canvas-dock");
|
|
12
|
+
const create_category_drawer_1 = require("./create-category-drawer");
|
|
13
|
+
const edit_category_drawer_1 = require("./edit-category-drawer");
|
|
14
|
+
const tree_state_1 = require("./lib/tree-state");
|
|
15
|
+
const layout_engine_1 = require("./lib/layout-engine");
|
|
16
|
+
const sdk_1 = require("../../lib/sdk");
|
|
17
|
+
const nodeTypes = {
|
|
18
|
+
categoryNode: category_node_1.CategoryNode,
|
|
19
|
+
};
|
|
20
|
+
const edgeTypes = {
|
|
21
|
+
smoothstep: custom_edge_1.CustomSmoothStepEdge,
|
|
22
|
+
};
|
|
23
|
+
const CanvasInner = () => {
|
|
24
|
+
const { fitView, zoomIn, zoomOut } = (0, react_2.useReactFlow)();
|
|
25
|
+
const [originalCategories, setOriginalCategories] = (0, react_1.useState)([]);
|
|
26
|
+
const [categories, setCategories] = (0, react_1.useState)([]);
|
|
27
|
+
const [salesChannels, setSalesChannels] = (0, react_1.useState)([]);
|
|
28
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
29
|
+
const [isSaving, setIsSaving] = (0, react_1.useState)(false);
|
|
30
|
+
const [direction, setDirection] = (0, react_1.useState)("TB");
|
|
31
|
+
const [selectedStorefront, setSelectedStorefront] = (0, react_1.useState)("all");
|
|
32
|
+
const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
|
|
33
|
+
const [collapsedIds, setCollapsedIds] = (0, react_1.useState)(new Set());
|
|
34
|
+
const [createDrawerOpen, setCreateDrawerOpen] = (0, react_1.useState)(false);
|
|
35
|
+
const [createParentCategory, setCreateParentCategory] = (0, react_1.useState)(null);
|
|
36
|
+
const [editDrawerOpen, setEditDrawerOpen] = (0, react_1.useState)(false);
|
|
37
|
+
const [editingCategory, setEditingCategory] = (0, react_1.useState)(null);
|
|
38
|
+
const [nodes, setNodes, onNodesChange] = (0, react_2.useNodesState)([]);
|
|
39
|
+
const [edges, setEdges, onEdgesChange] = (0, react_2.useEdgesState)([]);
|
|
40
|
+
const loadCategories = (0, react_1.useCallback)(async () => {
|
|
41
|
+
setIsLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
const res = await sdk_1.sdk.client.fetch("/admin/category-tree");
|
|
44
|
+
setOriginalCategories(res.categories || []);
|
|
45
|
+
setCategories(res.categories || []);
|
|
46
|
+
setSalesChannels(res.sales_channels || []);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
ui_1.toast.error(err?.message || "Failed to load category tree");
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
(0, react_1.useEffect)(() => {
|
|
56
|
+
loadCategories();
|
|
57
|
+
}, [loadCategories]);
|
|
58
|
+
const { childrenMap, enrichedCategories } = (0, react_1.useMemo)(() => {
|
|
59
|
+
const map = new Map();
|
|
60
|
+
for (const c of categories) {
|
|
61
|
+
if (c.parent_category_id) {
|
|
62
|
+
const list = map.get(c.parent_category_id) || [];
|
|
63
|
+
list.push(c.id);
|
|
64
|
+
map.set(c.parent_category_id, list);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const tiers = (0, tree_state_1.computeTiers)(categories);
|
|
68
|
+
const siblingRanks = (0, tree_state_1.computeSiblingRanks)(categories);
|
|
69
|
+
const enriched = categories.map((cat) => {
|
|
70
|
+
const kids = map.get(cat.id) || [];
|
|
71
|
+
const rankInfo = siblingRanks.get(cat.id);
|
|
72
|
+
return {
|
|
73
|
+
...cat,
|
|
74
|
+
tier: tiers.get(cat.id) || 1,
|
|
75
|
+
sibling_rank: rankInfo?.rankNum || 1,
|
|
76
|
+
children_count: kids.length,
|
|
77
|
+
has_children: kids.length > 0,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
return { childrenMap: map, enrichedCategories: enriched };
|
|
81
|
+
}, [categories]);
|
|
82
|
+
const handleToggleCollapse = (0, react_1.useCallback)((id) => {
|
|
83
|
+
setCollapsedIds((prev) => {
|
|
84
|
+
const next = new Set(prev);
|
|
85
|
+
if (next.has(id)) {
|
|
86
|
+
next.delete(id);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
next.add(id);
|
|
90
|
+
}
|
|
91
|
+
return next;
|
|
92
|
+
});
|
|
93
|
+
}, []);
|
|
94
|
+
const handleAddChild = (0, react_1.useCallback)((parent) => {
|
|
95
|
+
setCreateParentCategory(parent);
|
|
96
|
+
setCreateDrawerOpen(true);
|
|
97
|
+
}, []);
|
|
98
|
+
const handleEdit = (0, react_1.useCallback)((category) => {
|
|
99
|
+
setEditingCategory(category);
|
|
100
|
+
setEditDrawerOpen(true);
|
|
101
|
+
}, []);
|
|
102
|
+
const handleDetach = (0, react_1.useCallback)((id) => {
|
|
103
|
+
setCategories((prev) => prev.map((c) => (c.id === id ? { ...c, parent_category_id: null } : c)));
|
|
104
|
+
}, []);
|
|
105
|
+
(0, react_1.useEffect)(() => {
|
|
106
|
+
if (enrichedCategories.length === 0) {
|
|
107
|
+
setNodes([]);
|
|
108
|
+
setEdges([]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const siblingRankMap = (0, tree_state_1.computeSiblingRanks)(enrichedCategories);
|
|
112
|
+
const query = searchQuery.trim().toLowerCase();
|
|
113
|
+
const rawNodes = enrichedCategories.map((cat) => {
|
|
114
|
+
const rankInfo = siblingRankMap.get(cat.id);
|
|
115
|
+
const matchesSearch = !query ||
|
|
116
|
+
cat.name.toLowerCase().includes(query) ||
|
|
117
|
+
cat.handle.toLowerCase().includes(query);
|
|
118
|
+
const matchesStorefront = selectedStorefront === "all" ||
|
|
119
|
+
(cat.sales_channels || []).includes(selectedStorefront);
|
|
120
|
+
const isDimmed = !matchesSearch || !matchesStorefront;
|
|
121
|
+
return {
|
|
122
|
+
id: cat.id,
|
|
123
|
+
type: "categoryNode",
|
|
124
|
+
position: { x: 0, y: 0 },
|
|
125
|
+
data: {
|
|
126
|
+
category: cat,
|
|
127
|
+
tier: cat.tier,
|
|
128
|
+
siblingRank: rankInfo?.rankNum || 1,
|
|
129
|
+
superscriptRank: rankInfo?.superscript || "¹",
|
|
130
|
+
salesChannels,
|
|
131
|
+
isCollapsed: collapsedIds.has(cat.id),
|
|
132
|
+
hiddenDescendantCount: cat.children_count,
|
|
133
|
+
direction,
|
|
134
|
+
isDimmed,
|
|
135
|
+
onToggleCollapse: handleToggleCollapse,
|
|
136
|
+
onAddChild: handleAddChild,
|
|
137
|
+
onEdit: handleEdit,
|
|
138
|
+
onDetach: handleDetach,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
const rawEdges = [];
|
|
143
|
+
for (const cat of enrichedCategories) {
|
|
144
|
+
if (cat.parent_category_id) {
|
|
145
|
+
rawEdges.push({
|
|
146
|
+
id: `e-${cat.parent_category_id}-${cat.id}`,
|
|
147
|
+
source: cat.parent_category_id,
|
|
148
|
+
target: cat.id,
|
|
149
|
+
type: "smoothstep",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
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 });
|
|
155
|
+
setNodes(layoutedNodes);
|
|
156
|
+
setEdges(layoutedEdges);
|
|
157
|
+
}, [
|
|
158
|
+
enrichedCategories,
|
|
159
|
+
collapsedIds,
|
|
160
|
+
childrenMap,
|
|
161
|
+
direction,
|
|
162
|
+
searchQuery,
|
|
163
|
+
selectedStorefront,
|
|
164
|
+
salesChannels,
|
|
165
|
+
handleToggleCollapse,
|
|
166
|
+
handleAddChild,
|
|
167
|
+
handleEdit,
|
|
168
|
+
handleDetach,
|
|
169
|
+
setNodes,
|
|
170
|
+
setEdges,
|
|
171
|
+
]);
|
|
172
|
+
(0, react_1.useEffect)(() => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
fitView({ padding: 0.2, duration: 400 });
|
|
175
|
+
}, 100);
|
|
176
|
+
return () => clearTimeout(timer);
|
|
177
|
+
}, [fitView, direction]);
|
|
178
|
+
const onConnect = (0, react_1.useCallback)((params) => {
|
|
179
|
+
if (!params.source || !params.target || params.source === params.target)
|
|
180
|
+
return;
|
|
181
|
+
if ((0, tree_state_1.wouldCreateCycle)(categories, params.target, params.source)) {
|
|
182
|
+
ui_1.toast.error("Cannot connect: would create a circular loop in the hierarchy!");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
setCategories((prev) => prev.map((c) => c.id === params.target ? { ...c, parent_category_id: params.source } : c));
|
|
186
|
+
}, [categories]);
|
|
187
|
+
const onEdgesDelete = (0, react_1.useCallback)((deletedEdges) => {
|
|
188
|
+
const targetIds = new Set(deletedEdges.map((e) => e.target));
|
|
189
|
+
setCategories((prev) => prev.map((c) => (targetIds.has(c.id) ? { ...c, parent_category_id: null } : c)));
|
|
190
|
+
}, []);
|
|
191
|
+
const handleExpandAll = () => setCollapsedIds(new Set());
|
|
192
|
+
const handleCollapseAll = () => {
|
|
193
|
+
const parents = new Set(categories.filter((c) => (childrenMap.get(c.id) || []).length > 0).map((c) => c.id));
|
|
194
|
+
setCollapsedIds(parents);
|
|
195
|
+
};
|
|
196
|
+
const handleFocusRoot = () => {
|
|
197
|
+
const rootIds = new Set(categories.filter((c) => !c.parent_category_id).map((c) => c.id));
|
|
198
|
+
setCollapsedIds(rootIds);
|
|
199
|
+
};
|
|
200
|
+
const dirtyMoves = (0, react_1.useMemo)(() => {
|
|
201
|
+
return (0, tree_state_1.computeMoves)(originalCategories, categories);
|
|
202
|
+
}, [originalCategories, categories]);
|
|
203
|
+
const handleSave = async () => {
|
|
204
|
+
if (dirtyMoves.length === 0)
|
|
205
|
+
return;
|
|
206
|
+
setIsSaving(true);
|
|
207
|
+
try {
|
|
208
|
+
await sdk_1.sdk.client.fetch("/admin/category-tree/reorder", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: { moves: dirtyMoves },
|
|
211
|
+
});
|
|
212
|
+
ui_1.toast.success("Category hierarchy successfully saved");
|
|
213
|
+
setOriginalCategories([...categories]);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
ui_1.toast.error(err?.message || "Failed to save category order");
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
setIsSaving(false);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const handleReset = () => {
|
|
223
|
+
setCategories([...originalCategories]);
|
|
224
|
+
ui_1.toast.info("Changes reset to previous saved state");
|
|
225
|
+
};
|
|
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: () => {
|
|
227
|
+
setCreateParentCategory(null);
|
|
228
|
+
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) => {
|
|
230
|
+
setCategories((prev) => [...prev, newCat]);
|
|
231
|
+
setOriginalCategories((prev) => [...prev, newCat]);
|
|
232
|
+
} }), (0, jsx_runtime_1.jsx)(edit_category_drawer_1.EditCategoryDrawer, { open: editDrawerOpen, onOpenChange: setEditDrawerOpen, category: editingCategory, salesChannels: salesChannels, onSuccess: (updatedCat) => {
|
|
233
|
+
setCategories((prev) => prev.map((c) => (c.id === updatedCat.id ? updatedCat : c)));
|
|
234
|
+
setOriginalCategories((prev) => prev.map((c) => (c.id === updatedCat.id ? updatedCat : c)));
|
|
235
|
+
}, onDeleteSuccess: (deletedId) => {
|
|
236
|
+
setCategories((prev) => prev.filter((c) => c.id !== deletedId));
|
|
237
|
+
setOriginalCategories((prev) => prev.filter((c) => c.id !== deletedId));
|
|
238
|
+
} })] }));
|
|
239
|
+
};
|
|
240
|
+
const CategoryTreeCanvas = () => {
|
|
241
|
+
return ((0, jsx_runtime_1.jsx)(react_2.ReactFlowProvider, { children: (0, jsx_runtime_1.jsx)(CanvasInner, {}) }));
|
|
242
|
+
};
|
|
243
|
+
exports.CategoryTreeCanvas = CategoryTreeCanvas;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CategoryTreeNode, SalesChannelInfo } from "./types";
|
|
3
|
+
export interface CreateCategoryDrawerProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
parentCategory: CategoryTreeNode | null;
|
|
7
|
+
salesChannels: SalesChannelInfo[];
|
|
8
|
+
onSuccess: (newCat: CategoryTreeNode) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const CreateCategoryDrawer: React.FC<CreateCategoryDrawerProps>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CreateCategoryDrawer = 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 sdk_1 = require("../../lib/sdk");
|
|
8
|
+
const CreateCategoryDrawer = ({ open, onOpenChange, parentCategory, salesChannels, onSuccess, }) => {
|
|
9
|
+
const [name, setName] = (0, react_1.useState)("");
|
|
10
|
+
const [handle, setHandle] = (0, react_1.useState)("");
|
|
11
|
+
const [description, setDescription] = (0, react_1.useState)("");
|
|
12
|
+
const [imageUrl, setImageUrl] = (0, react_1.useState)("");
|
|
13
|
+
const [isActive, setIsActive] = (0, react_1.useState)(true);
|
|
14
|
+
const [isInternal, setIsInternal] = (0, react_1.useState)(false);
|
|
15
|
+
const [selectedChannels, setSelectedChannels] = (0, react_1.useState)([]);
|
|
16
|
+
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
17
|
+
(0, react_1.useEffect)(() => {
|
|
18
|
+
if (open) {
|
|
19
|
+
setName("");
|
|
20
|
+
setHandle("");
|
|
21
|
+
setDescription("");
|
|
22
|
+
setImageUrl("");
|
|
23
|
+
setIsActive(true);
|
|
24
|
+
setIsInternal(false);
|
|
25
|
+
if (parentCategory?.sales_channels) {
|
|
26
|
+
setSelectedChannels(parentCategory.sales_channels);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
setSelectedChannels(salesChannels.map((sc) => sc.id));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, [open, parentCategory, salesChannels]);
|
|
33
|
+
const handleNameChange = (val) => {
|
|
34
|
+
setName(val);
|
|
35
|
+
if (!handle || handle === name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")) {
|
|
36
|
+
setHandle(val
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
39
|
+
.replace(/^-|-$/g, ""));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const toggleChannel = (channelId) => {
|
|
43
|
+
setSelectedChannels((prev) => prev.includes(channelId)
|
|
44
|
+
? prev.filter((id) => id !== channelId)
|
|
45
|
+
: [...prev, channelId]);
|
|
46
|
+
};
|
|
47
|
+
const handleSubmit = async (e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
if (!name.trim()) {
|
|
50
|
+
ui_1.toast.error("Category name is required");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setIsSubmitting(true);
|
|
54
|
+
try {
|
|
55
|
+
const res = await sdk_1.sdk.client.fetch("/admin/category-tree/create", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: {
|
|
58
|
+
name: name.trim(),
|
|
59
|
+
handle: handle.trim() || undefined,
|
|
60
|
+
description: description.trim() || undefined,
|
|
61
|
+
parent_category_id: parentCategory ? parentCategory.id : null,
|
|
62
|
+
image_url: imageUrl.trim() || undefined,
|
|
63
|
+
sales_channels: selectedChannels,
|
|
64
|
+
is_active: isActive,
|
|
65
|
+
is_internal: isInternal,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
ui_1.toast.success(`Category "${name}" created successfully`);
|
|
69
|
+
onSuccess(res.category);
|
|
70
|
+
onOpenChange(false);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
ui_1.toast.error(err?.message || "Failed to create category");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
setIsSubmitting(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
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-md", 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, { children: [(0, jsx_runtime_1.jsx)(ui_1.Drawer.Title, { children: parentCategory ? `New Subcategory under "${parentCategory.name}"` : "New Root Category" }), (0, jsx_runtime_1.jsx)(ui_1.Drawer.Description, { children: parentCategory
|
|
80
|
+
? `Will be created at Tier ${(parentCategory.tier || 1) + 1} and linked in the hierarchy.`
|
|
81
|
+
: "Creates a top-level Tier 1 main category in the store." })] }), (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: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "cat-name", className: "text-xs font-semibold", children: "Category Name *" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "cat-name", size: "small", placeholder: "e.g. Embroidery Thread", value: name, onChange: (e) => handleNameChange(e.target.value), autoFocus: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "cat-handle", className: "text-xs font-semibold", children: "Handle (URL slug)" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "cat-handle", size: "small", placeholder: "e.g. embroidery-thread", value: handle, onChange: (e) => setHandle(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: "cat-image", className: "text-xs font-semibold", children: "Category Image URL" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "cat-image", size: "small", placeholder: "https://... or /images/...", value: imageUrl, onChange: (e) => setImageUrl(e.target.value) }), imageUrl && ((0, jsx_runtime_1.jsx)("div", { className: "mt-1 w-16 h-16 rounded-lg overflow-hidden border border-ui-border-base bg-ui-bg-subtle", children: (0, jsx_runtime_1.jsx)("img", { src: imageUrl, alt: "Preview", className: "w-full h-full object-cover", onError: (e) => {
|
|
82
|
+
;
|
|
83
|
+
e.target.style.display = "none";
|
|
84
|
+
} }) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "cat-desc", className: "text-xs font-semibold", children: "Description" }), (0, jsx_runtime_1.jsx)(ui_1.Textarea, { id: "cat-desc", placeholder: "Brief summary of this category...", value: description, onChange: (e) => setDescription(e.target.value), rows: 2 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2 pt-2 border-t border-ui-border-base", 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 storefronts can display this category." }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2 mt-1", children: salesChannels.map((channel) => {
|
|
85
|
+
const isChecked = selectedChannels.includes(channel.id);
|
|
86
|
+
return ((0, jsx_runtime_1.jsxs)("label", { className: "flex items-center gap-2.5 text-xs text-ui-fg-base cursor-pointer select-none", 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));
|
|
87
|
+
}) })] }), (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.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, children: "Cancel" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { type: "submit", size: "small", isLoading: isSubmitting, disabled: isSubmitting || !name.trim(), children: "Create Category" })] })] }) }) }));
|
|
88
|
+
};
|
|
89
|
+
exports.CreateCategoryDrawer = CreateCategoryDrawer;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CustomSmoothStepEdge = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_2 = require("@xyflow/react");
|
|
7
|
+
exports.CustomSmoothStepEdge = (0, react_1.memo)((props) => {
|
|
8
|
+
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerEnd, selected, } = props;
|
|
9
|
+
const [edgePath] = (0, react_2.getSmoothStepPath)({
|
|
10
|
+
sourceX,
|
|
11
|
+
sourceY,
|
|
12
|
+
sourcePosition,
|
|
13
|
+
targetX,
|
|
14
|
+
targetY,
|
|
15
|
+
targetPosition,
|
|
16
|
+
borderRadius: 16,
|
|
17
|
+
});
|
|
18
|
+
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
|
+
...style,
|
|
20
|
+
stroke: selected ? "var(--bg-interactive, #3b82f6)" : "var(--border-strong, #cbd5e1)",
|
|
21
|
+
strokeWidth: selected ? 2.5 : 1.75,
|
|
22
|
+
transition: "stroke 0.2s, stroke-width 0.2s",
|
|
23
|
+
} })] }));
|
|
24
|
+
});
|
|
25
|
+
exports.CustomSmoothStepEdge.displayName = "CustomSmoothStepEdge";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CategoryTreeNode, SalesChannelInfo } from "./types";
|
|
3
|
+
export interface EditCategoryDrawerProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
category: CategoryTreeNode | null;
|
|
7
|
+
salesChannels: SalesChannelInfo[];
|
|
8
|
+
onSuccess: (updatedCat: CategoryTreeNode) => void;
|
|
9
|
+
onDeleteSuccess: (deletedId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare const EditCategoryDrawer: React.FC<EditCategoryDrawerProps>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EditCategoryDrawer = 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 EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuccess, onDeleteSuccess, }) => {
|
|
10
|
+
const [name, setName] = (0, react_1.useState)("");
|
|
11
|
+
const [handle, setHandle] = (0, react_1.useState)("");
|
|
12
|
+
const [description, setDescription] = (0, react_1.useState)("");
|
|
13
|
+
const [imageUrl, setImageUrl] = (0, react_1.useState)("");
|
|
14
|
+
const [isActive, setIsActive] = (0, react_1.useState)(true);
|
|
15
|
+
const [isInternal, setIsInternal] = (0, react_1.useState)(false);
|
|
16
|
+
const [selectedChannels, setSelectedChannels] = (0, react_1.useState)([]);
|
|
17
|
+
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
18
|
+
const [isDeleting, setIsDeleting] = (0, react_1.useState)(false);
|
|
19
|
+
(0, react_1.useEffect)(() => {
|
|
20
|
+
if (category) {
|
|
21
|
+
setName(category.name || "");
|
|
22
|
+
setHandle(category.handle || "");
|
|
23
|
+
setDescription(category.description || "");
|
|
24
|
+
setImageUrl(category.image_url || "");
|
|
25
|
+
setIsActive(Boolean(category.is_active));
|
|
26
|
+
setIsInternal(Boolean(category.is_internal));
|
|
27
|
+
setSelectedChannels(category.sales_channels || salesChannels.map((s) => s.id));
|
|
28
|
+
}
|
|
29
|
+
}, [category, salesChannels]);
|
|
30
|
+
if (!category)
|
|
31
|
+
return null;
|
|
32
|
+
const toggleChannel = (channelId) => {
|
|
33
|
+
setSelectedChannels((prev) => prev.includes(channelId)
|
|
34
|
+
? prev.filter((id) => id !== channelId)
|
|
35
|
+
: [...prev, channelId]);
|
|
36
|
+
};
|
|
37
|
+
const handleSubmit = async (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
if (!name.trim()) {
|
|
40
|
+
ui_1.toast.error("Category name is required");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setIsSubmitting(true);
|
|
44
|
+
try {
|
|
45
|
+
const updatedMetadata = {
|
|
46
|
+
...(category.metadata || {}),
|
|
47
|
+
sales_channels: selectedChannels,
|
|
48
|
+
};
|
|
49
|
+
if (imageUrl.trim()) {
|
|
50
|
+
updatedMetadata.thumbnail = imageUrl.trim();
|
|
51
|
+
updatedMetadata.image = imageUrl.trim();
|
|
52
|
+
}
|
|
53
|
+
await sdk_1.sdk.admin.productCategory.update(category.id, {
|
|
54
|
+
name: name.trim(),
|
|
55
|
+
handle: handle.trim() || undefined,
|
|
56
|
+
description: description.trim() || undefined,
|
|
57
|
+
is_active: isActive,
|
|
58
|
+
is_internal: isInternal,
|
|
59
|
+
metadata: updatedMetadata,
|
|
60
|
+
});
|
|
61
|
+
const updated = {
|
|
62
|
+
...category,
|
|
63
|
+
name: name.trim(),
|
|
64
|
+
handle: handle.trim(),
|
|
65
|
+
description: description.trim() || null,
|
|
66
|
+
image_url: imageUrl.trim() || category.image_url,
|
|
67
|
+
sales_channels: selectedChannels,
|
|
68
|
+
is_active: isActive,
|
|
69
|
+
is_internal: isInternal,
|
|
70
|
+
metadata: updatedMetadata,
|
|
71
|
+
};
|
|
72
|
+
ui_1.toast.success("Category updated successfully");
|
|
73
|
+
onSuccess(updated);
|
|
74
|
+
onOpenChange(false);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
ui_1.toast.error(err?.message || "Failed to update category");
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
setIsSubmitting(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const handleDelete = async () => {
|
|
84
|
+
if (!confirm(`Are you sure you want to delete category "${category.name}"? Subcategories will be promoted.`)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
setIsDeleting(true);
|
|
88
|
+
try {
|
|
89
|
+
await sdk_1.sdk.admin.productCategory.delete(category.id);
|
|
90
|
+
ui_1.toast.success(`Category "${category.name}" deleted`);
|
|
91
|
+
onDeleteSuccess(category.id);
|
|
92
|
+
onOpenChange(false);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
ui_1.toast.error(err?.message || "Failed to delete category");
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setIsDeleting(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
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-md", 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, { children: [(0, jsx_runtime_1.jsxs)(ui_1.Drawer.Title, { children: ["Edit Category: ", category.name] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Description, { children: ["Tier ", category.tier, " \u2022 ID: ", category.id] })] }), (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: [(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", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-image", className: "text-xs font-semibold", children: "Category Image URL" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-image", size: "small", value: imageUrl, onChange: (e) => setImageUrl(e.target.value) }), imageUrl && ((0, jsx_runtime_1.jsx)("div", { className: "mt-1 w-16 h-16 rounded-lg overflow-hidden border border-ui-border-base bg-ui-bg-subtle", children: (0, jsx_runtime_1.jsx)("img", { src: imageUrl, alt: "Category", className: "w-full h-full object-cover", onError: (e) => {
|
|
102
|
+
;
|
|
103
|
+
e.target.style.display = "none";
|
|
104
|
+
} }) }))] }), (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: 2 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2 pt-2 border-t border-ui-border-base", 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 storefronts can display this category." }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2 mt-1", children: salesChannels.map((channel) => {
|
|
105
|
+
const isChecked = selectedChannels.includes(channel.id);
|
|
106
|
+
return ((0, jsx_runtime_1.jsxs)("label", { className: "flex items-center gap-2.5 text-xs text-ui-fg-base cursor-pointer select-none", 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));
|
|
107
|
+
}) })] }), (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" })] }) })] }), (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: "Cancel" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { type: "submit", size: "small", isLoading: isSubmitting, disabled: isSubmitting || !name.trim() || isDeleting, children: "Save Changes" })] })] }) }) }));
|
|
108
|
+
};
|
|
109
|
+
exports.EditCategoryDrawer = EditCategoryDrawer;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Node, Edge } from "@xyflow/react";
|
|
2
|
+
export declare const CARD_WIDTH = 270;
|
|
3
|
+
export declare const CARD_HEIGHT = 86;
|
|
4
|
+
export interface LayoutOptions {
|
|
5
|
+
direction?: "TB" | "LR";
|
|
6
|
+
nodeSep?: number;
|
|
7
|
+
rankSep?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function computeDagreLayout(nodes: Node[], edges: Edge[], options?: LayoutOptions): {
|
|
10
|
+
nodes: Node[];
|
|
11
|
+
edges: Edge[];
|
|
12
|
+
};
|