@hectasquare/medusa-plugin-category-tree 1.0.1 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/components/category-tree/canvas-dock.d.ts +5 -0
- package/.medusa/server/src/admin/components/category-tree/canvas-dock.js +4 -2
- package/.medusa/server/src/admin/components/category-tree/category-node.js +12 -2
- package/.medusa/server/src/admin/components/category-tree/category-products-manager.d.ts +8 -0
- package/.medusa/server/src/admin/components/category-tree/category-products-manager.js +290 -0
- package/.medusa/server/src/admin/components/category-tree/category-tree-canvas.js +45 -3
- package/.medusa/server/src/admin/components/category-tree/custom-edge.js +5 -3
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +89 -8
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.d.ts +9 -1
- package/.medusa/server/src/admin/components/category-tree/lib/layout-engine.js +124 -30
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.d.ts +14 -0
- package/.medusa/server/src/admin/components/category-tree/lib/tree-state.js +95 -0
- package/.medusa/server/src/admin/components/category-tree/tier-bars.d.ts +4 -2
- package/.medusa/server/src/admin/components/category-tree/tier-bars.js +9 -4
- package/.medusa/server/src/admin/components/category-tree/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/admin/components/category-tree/canvas-dock.tsx +71 -18
- package/src/admin/components/category-tree/category-node.tsx +48 -9
- package/src/admin/components/category-tree/category-products-manager.tsx +749 -0
- package/src/admin/components/category-tree/category-tree-canvas.tsx +59 -2
- package/src/admin/components/category-tree/custom-edge.tsx +6 -2
- package/src/admin/components/category-tree/edit-category-drawer.tsx +413 -118
- package/src/admin/components/category-tree/lib/layout-engine.ts +161 -25
- package/src/admin/components/category-tree/lib/tree-state.ts +112 -0
- package/src/admin/components/category-tree/tier-bars.tsx +12 -5
- package/src/admin/components/category-tree/types.ts +4 -0
|
@@ -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-
|
|
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"}`,
|
|
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.
|
|
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,
|
|
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 ? "
|
|
21
|
-
strokeWidth: selected ? 2.
|
|
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
|
});
|