@hectasquare/medusa-plugin-category-tree 1.0.10 → 1.0.11
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/category-products-manager.js +57 -17
- package/.medusa/server/src/admin/components/category-tree/edit-category-drawer.js +116 -46
- package/package.json +1 -1
- package/src/admin/components/category-tree/category-products-manager.tsx +88 -29
- package/src/admin/components/category-tree/edit-category-drawer.tsx +156 -76
|
@@ -155,7 +155,7 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
155
155
|
const currentOrder = products.map((p) => p.id);
|
|
156
156
|
await persistProductConfig(currentOrder, next);
|
|
157
157
|
};
|
|
158
|
-
// Remove products from category
|
|
158
|
+
// Remove products from category using official Medusa v2 JS SDK method
|
|
159
159
|
const handleRemoveProducts = async (idsToRemove) => {
|
|
160
160
|
if (idsToRemove.length === 0)
|
|
161
161
|
return;
|
|
@@ -166,9 +166,8 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
166
166
|
return;
|
|
167
167
|
setIsRemoving(true);
|
|
168
168
|
try {
|
|
169
|
-
await sdk_1.sdk.
|
|
170
|
-
|
|
171
|
-
body: { remove: idsToRemove },
|
|
169
|
+
await sdk_1.sdk.admin.productCategory.updateProducts(categoryId, {
|
|
170
|
+
remove: idsToRemove,
|
|
172
171
|
});
|
|
173
172
|
ui_1.toast.success(`Removed ${idsToRemove.length} product(s) from category`);
|
|
174
173
|
setSelectedIds(new Set());
|
|
@@ -210,15 +209,15 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
210
209
|
searchCatalog(searchQuery, catalogPage);
|
|
211
210
|
}
|
|
212
211
|
}, [focusModalOpen, searchQuery, catalogPage, searchCatalog]);
|
|
212
|
+
// Save added products using official Medusa v2 JS SDK method
|
|
213
213
|
const handleSaveAddedProducts = async () => {
|
|
214
214
|
const idsToAdd = Array.from(catalogSelectedIds);
|
|
215
215
|
if (idsToAdd.length === 0)
|
|
216
216
|
return;
|
|
217
217
|
setIsAdding(true);
|
|
218
218
|
try {
|
|
219
|
-
await sdk_1.sdk.
|
|
220
|
-
|
|
221
|
-
body: { add: idsToAdd },
|
|
219
|
+
await sdk_1.sdk.admin.productCategory.updateProducts(categoryId, {
|
|
220
|
+
add: idsToAdd,
|
|
222
221
|
});
|
|
223
222
|
ui_1.toast.success(`Added ${idsToAdd.length} product(s) to ${categoryName}`);
|
|
224
223
|
setFocusModalOpen(false);
|
|
@@ -243,18 +242,39 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
243
242
|
}
|
|
244
243
|
return products;
|
|
245
244
|
}, [products, activeView, topSellerIds]);
|
|
246
|
-
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: () =>
|
|
245
|
+
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: (e) => {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
e.stopPropagation();
|
|
248
|
+
setActiveView("all");
|
|
249
|
+
}, className: `px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${activeView === "all"
|
|
247
250
|
? "bg-ui-bg-base text-ui-fg-base shadow-2xs font-semibold"
|
|
248
|
-
: "text-ui-fg-muted hover:text-ui-fg-base"}`, children: ["All Products (", products.length, ")"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () =>
|
|
251
|
+
: "text-ui-fg-muted hover:text-ui-fg-base"}`, children: ["All Products (", products.length, ")"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: (e) => {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
e.stopPropagation();
|
|
254
|
+
setActiveView("top_sellers");
|
|
255
|
+
}, className: `flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${activeView === "top_sellers"
|
|
249
256
|
? "bg-amber-500/10 text-amber-700 dark:text-amber-400 font-semibold"
|
|
250
|
-
: "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: () =>
|
|
257
|
+
: "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, { type: "button", size: "small", variant: "danger", disabled: isRemoving, onClick: (e) => {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
handleRemoveProducts(Array.from(selectedIds));
|
|
261
|
+
}, 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, { type: "button", size: "small", variant: "secondary", onClick: (e) => {
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
e.stopPropagation();
|
|
251
264
|
setCatalogSelectedIds(new Set());
|
|
252
265
|
setSearchQuery("");
|
|
253
266
|
setCatalogPage(0);
|
|
254
267
|
setFocusModalOpen(true);
|
|
255
268
|
}, 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"
|
|
256
269
|
? "No products marked as Top Commercial Sellers yet. Star a product below to highlight it."
|
|
257
|
-
: "No products assigned to this category." }), activeView === "all" && ((0, jsx_runtime_1.jsxs)(ui_1.Button, {
|
|
270
|
+
: "No products assigned to this category." }), activeView === "all" && ((0, jsx_runtime_1.jsxs)(ui_1.Button, { type: "button", size: "small", variant: "secondary", onClick: (e) => {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
e.stopPropagation();
|
|
273
|
+
setCatalogSelectedIds(new Set());
|
|
274
|
+
setSearchQuery("");
|
|
275
|
+
setCatalogPage(0);
|
|
276
|
+
setFocusModalOpen(true);
|
|
277
|
+
}, 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: () => {
|
|
258
278
|
if (selectedIds.size === displayedProducts.length)
|
|
259
279
|
setSelectedIds(new Set());
|
|
260
280
|
else
|
|
@@ -279,22 +299,26 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
279
299
|
e.preventDefault();
|
|
280
300
|
e.stopPropagation();
|
|
281
301
|
handleMoveProduct(index, "down");
|
|
282
|
-
}, className: "text-[9px] text-ui-fg-muted hover:text-ui-fg-base disabled:opacity-20 px-0.5 leading-none cursor-pointer", 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)("
|
|
302
|
+
}, className: "text-[9px] text-ui-fg-muted hover:text-ui-fg-base disabled:opacity-20 px-0.5 leading-none cursor-pointer", 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)("a", { href: `/app/products/${p.id}`, target: "_blank", rel: "noopener noreferrer", 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 hover:opacity-80 transition-opacity", title: `Open ${p.title} in new tab`, 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.jsxs)("a", { href: `/app/products/${p.id}`, target: "_blank", rel: "noopener noreferrer", className: "font-semibold text-ui-fg-base hover:text-ui-fg-interactive truncate leading-snug hover:underline flex items-center gap-1 group/pname", title: `Open ${p.title} in new tab`, children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate", children: p.title }), (0, jsx_runtime_1.jsx)(icons_1.ArrowUpRightMicro, { className: "w-3.5 h-3.5 text-ui-fg-muted group-hover/pname:text-ui-fg-interactive transition-colors shrink-0" })] }) })] }) }), (0, jsx_runtime_1.jsx)("td", { className: "py-2.5 px-3", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: (e) => {
|
|
283
303
|
e.preventDefault();
|
|
284
304
|
e.stopPropagation();
|
|
285
305
|
handleToggleTopSeller(p.id, p.title);
|
|
286
|
-
}, className: `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border transition-all ${isTopSeller
|
|
306
|
+
}, className: `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border transition-all cursor-pointer ${isTopSeller
|
|
287
307
|
? "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-300 dark:border-amber-700 shadow-2xs"
|
|
288
308
|
: "bg-ui-bg-subtle text-ui-fg-muted border-ui-border-base hover:text-amber-600 hover:border-amber-300"}`, title: isTopSeller
|
|
289
309
|
? "Click to remove from Top Commercial Sellers"
|
|
290
|
-
: "Click to mark as Top Commercial Seller (Reflected
|
|
310
|
+
: "Click to mark as Top Commercial Seller (Reflected on 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"
|
|
291
311
|
? "bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300"
|
|
292
312
|
: "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: (e) => {
|
|
293
313
|
e.preventDefault();
|
|
294
314
|
e.stopPropagation();
|
|
295
315
|
handleRemoveProducts([p.id]);
|
|
296
|
-
}, 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));
|
|
297
|
-
}) })] })) }), 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: (
|
|
316
|
+
}, className: "p-1 text-ui-fg-muted hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded transition-colors cursor-pointer", children: (0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3.5 h-3.5" }) }) })] }, p.id));
|
|
317
|
+
}) })] })) }), 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: (e) => {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
setFocusModalOpen(false);
|
|
321
|
+
}, className: "p-1.5 rounded-lg text-ui-fg-muted hover:text-ui-fg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer", 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) => {
|
|
298
322
|
setSearchQuery(e.target.value);
|
|
299
323
|
setCatalogPage(0);
|
|
300
324
|
}, 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 &&
|
|
@@ -338,6 +362,22 @@ const CategoryProductsManager = ({ categoryId, categoryName, initialMetadata, on
|
|
|
338
362
|
}, 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"
|
|
339
363
|
? "bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300"
|
|
340
364
|
: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"}`, children: p.status }) })] }, p.id));
|
|
341
|
-
})) })] }) }) }), (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: () =>
|
|
365
|
+
})) })] }) }) }), (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: (e) => {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
e.stopPropagation();
|
|
368
|
+
setCatalogPage((p) => Math.max(0, p - 1));
|
|
369
|
+
}, 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 cursor-pointer", children: "Prev" }), (0, jsx_runtime_1.jsx)("button", { type: "button", disabled: (catalogPage + 1) * PAGE_SIZE >= catalogTotalCount, onClick: (e) => {
|
|
370
|
+
e.preventDefault();
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
setCatalogPage((p) => p + 1);
|
|
373
|
+
}, 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 cursor-pointer", 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, { type: "button", size: "small", variant: "secondary", onClick: (e) => {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
e.stopPropagation();
|
|
376
|
+
setFocusModalOpen(false);
|
|
377
|
+
}, children: "Cancel" }), (0, jsx_runtime_1.jsxs)(ui_1.Button, { type: "button", size: "small", variant: "primary", disabled: catalogSelectedIds.size === 0 || isAdding, onClick: (e) => {
|
|
378
|
+
e.preventDefault();
|
|
379
|
+
e.stopPropagation();
|
|
380
|
+
handleSaveAddedProducts();
|
|
381
|
+
}, 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" })] })] })] })] }))] }));
|
|
342
382
|
};
|
|
343
383
|
exports.CategoryProductsManager = CategoryProductsManager;
|
|
@@ -93,8 +93,13 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
93
93
|
catch {
|
|
94
94
|
// Endpoint fallback
|
|
95
95
|
}
|
|
96
|
-
//
|
|
97
|
-
if (metadata
|
|
96
|
+
// Explicit metadata.images check: if metadata.images array exists (even if empty)
|
|
97
|
+
if (metadata && Array.isArray(metadata.images)) {
|
|
98
|
+
if (metadata.images.length === 0) {
|
|
99
|
+
setCategoryImages([]);
|
|
100
|
+
setImageUrl("");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
98
103
|
setCategoryImages(metadata.images);
|
|
99
104
|
const thumb = metadata.images.find((img) => img.type === "thumbnail") || metadata.images[0];
|
|
100
105
|
if (thumb?.url)
|
|
@@ -103,7 +108,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
103
108
|
}
|
|
104
109
|
// Legacy fallback
|
|
105
110
|
const list = [];
|
|
106
|
-
const thumb =
|
|
111
|
+
const thumb = metadata?.thumbnail || metadata?.image_url || currentImageUrl;
|
|
107
112
|
if (thumb) {
|
|
108
113
|
list.push({ url: thumb, type: "thumbnail" });
|
|
109
114
|
}
|
|
@@ -228,8 +233,26 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
228
233
|
setManualImageUrl("");
|
|
229
234
|
ui_1.toast.success(`Added to gallery as ${newType === "thumbnail" ? "Thumbnail" : newType === "banner" ? "Hero Banner" : "Image"}`);
|
|
230
235
|
};
|
|
231
|
-
// Delete category image
|
|
236
|
+
// Delete category image with immediate persistence & explicit nulls
|
|
232
237
|
const handleDeleteImage = async (img) => {
|
|
238
|
+
const remaining = categoryImages.filter((i) => i.url !== img.url && (img.id ? i.id !== img.id : true));
|
|
239
|
+
const nextThumb = remaining.find((i) => i.type === "thumbnail") || remaining[0] || null;
|
|
240
|
+
const nextBanner = remaining.find((i) => i.type === "banner") ||
|
|
241
|
+
(remaining.length > 1 ? remaining.find((i) => i.url !== nextThumb?.url) : null) ||
|
|
242
|
+
null;
|
|
243
|
+
// Update local state immediately
|
|
244
|
+
setCategoryImages(remaining);
|
|
245
|
+
setImageUrl(nextThumb ? nextThumb.url : "");
|
|
246
|
+
const updatedMeta = {
|
|
247
|
+
...(currentMetadata || {}),
|
|
248
|
+
images: remaining,
|
|
249
|
+
thumbnail: nextThumb ? nextThumb.url : null,
|
|
250
|
+
image_url: nextThumb ? nextThumb.url : null,
|
|
251
|
+
banner: nextBanner ? nextBanner.url : null,
|
|
252
|
+
image: (nextBanner?.url || nextThumb?.url) || null,
|
|
253
|
+
};
|
|
254
|
+
setCurrentMetadata(updatedMeta);
|
|
255
|
+
// Call backend endpoint if DB relation exists
|
|
233
256
|
if (img.id) {
|
|
234
257
|
try {
|
|
235
258
|
await sdk_1.sdk.client.fetch(`/admin/categories/${category.id}/images/batch`, {
|
|
@@ -240,16 +263,27 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
240
263
|
},
|
|
241
264
|
});
|
|
242
265
|
}
|
|
243
|
-
catch {
|
|
244
|
-
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.warn("Could not delete from backend images endpoint:", err);
|
|
245
268
|
}
|
|
246
269
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
270
|
+
// Immediately persist metadata changes to Medusa
|
|
271
|
+
try {
|
|
272
|
+
await sdk_1.sdk.admin.productCategory.update(category.id, {
|
|
273
|
+
metadata: updatedMeta,
|
|
274
|
+
});
|
|
275
|
+
const updatedCat = {
|
|
276
|
+
...category,
|
|
277
|
+
image_url: nextThumb ? nextThumb.url : "",
|
|
278
|
+
metadata: updatedMeta,
|
|
279
|
+
};
|
|
280
|
+
onSuccess(updatedCat);
|
|
281
|
+
ui_1.toast.success("Image removed and changes saved");
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
console.warn("Could not immediately update category metadata on delete:", err);
|
|
285
|
+
ui_1.toast.info("Image removed from gallery (remember to save)");
|
|
251
286
|
}
|
|
252
|
-
ui_1.toast.info("Image removed");
|
|
253
287
|
};
|
|
254
288
|
if (!category)
|
|
255
289
|
return null;
|
|
@@ -258,7 +292,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
258
292
|
? prev.filter((id) => id !== channelId)
|
|
259
293
|
: [...prev, channelId]);
|
|
260
294
|
};
|
|
261
|
-
const handleAddYoutube = () => {
|
|
295
|
+
const handleAddYoutube = async () => {
|
|
262
296
|
const raw = newYoutubeInput.trim();
|
|
263
297
|
if (!raw)
|
|
264
298
|
return;
|
|
@@ -272,15 +306,55 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
272
306
|
ui_1.toast.error("Video already added");
|
|
273
307
|
return;
|
|
274
308
|
}
|
|
275
|
-
|
|
309
|
+
const nextUrls = [...youtubeUrls, cleanUrl];
|
|
310
|
+
setYoutubeUrls(nextUrls);
|
|
276
311
|
setNewYoutubeInput("");
|
|
277
|
-
|
|
312
|
+
const updatedMeta = {
|
|
313
|
+
...(currentMetadata || {}),
|
|
314
|
+
youtube_urls: nextUrls,
|
|
315
|
+
youtube_url: nextUrls[0] || null,
|
|
316
|
+
};
|
|
317
|
+
setCurrentMetadata(updatedMeta);
|
|
318
|
+
try {
|
|
319
|
+
await sdk_1.sdk.admin.productCategory.update(category.id, {
|
|
320
|
+
metadata: updatedMeta,
|
|
321
|
+
});
|
|
322
|
+
onSuccess({
|
|
323
|
+
...category,
|
|
324
|
+
metadata: updatedMeta,
|
|
325
|
+
});
|
|
326
|
+
ui_1.toast.success("YouTube video added and saved");
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
ui_1.toast.success("YouTube video added");
|
|
330
|
+
}
|
|
278
331
|
};
|
|
279
|
-
const handleRemoveYoutube = (urlToRemove) => {
|
|
280
|
-
|
|
332
|
+
const handleRemoveYoutube = async (urlToRemove) => {
|
|
333
|
+
const nextUrls = youtubeUrls.filter((u) => u !== urlToRemove);
|
|
334
|
+
setYoutubeUrls(nextUrls);
|
|
335
|
+
const updatedMeta = {
|
|
336
|
+
...(currentMetadata || {}),
|
|
337
|
+
youtube_urls: nextUrls,
|
|
338
|
+
youtube_url: nextUrls[0] || null,
|
|
339
|
+
};
|
|
340
|
+
setCurrentMetadata(updatedMeta);
|
|
341
|
+
try {
|
|
342
|
+
await sdk_1.sdk.admin.productCategory.update(category.id, {
|
|
343
|
+
metadata: updatedMeta,
|
|
344
|
+
});
|
|
345
|
+
onSuccess({
|
|
346
|
+
...category,
|
|
347
|
+
metadata: updatedMeta,
|
|
348
|
+
});
|
|
349
|
+
ui_1.toast.success("Video removed and changes saved");
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
ui_1.toast.info("Video removed (remember to save)");
|
|
353
|
+
}
|
|
281
354
|
};
|
|
282
355
|
const handleSubmit = async (e) => {
|
|
283
|
-
e.preventDefault
|
|
356
|
+
if (e && e.preventDefault)
|
|
357
|
+
e.preventDefault();
|
|
284
358
|
if (!name.trim()) {
|
|
285
359
|
ui_1.toast.error("Category name is required");
|
|
286
360
|
return;
|
|
@@ -288,26 +362,22 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
288
362
|
setIsSubmitting(true);
|
|
289
363
|
try {
|
|
290
364
|
const thumbImg = categoryImages.find((img) => img.type === "thumbnail") ||
|
|
291
|
-
categoryImages[0]
|
|
365
|
+
categoryImages[0] ||
|
|
366
|
+
null;
|
|
292
367
|
const bannerImg = categoryImages.find((img) => img.type === "banner") ||
|
|
293
|
-
categoryImages.find((img) => img.url !== thumbImg?.url)
|
|
368
|
+
(categoryImages.length > 1 ? categoryImages.find((img) => img.url !== thumbImg?.url) : null) ||
|
|
369
|
+
null;
|
|
294
370
|
const updatedMetadata = {
|
|
295
371
|
...(currentMetadata || {}),
|
|
296
372
|
sales_channels: selectedChannels,
|
|
297
373
|
youtube_urls: youtubeUrls,
|
|
374
|
+
youtube_url: youtubeUrls[0] || null,
|
|
298
375
|
images: categoryImages,
|
|
376
|
+
thumbnail: thumbImg ? thumbImg.url : null,
|
|
377
|
+
image_url: thumbImg ? thumbImg.url : null,
|
|
378
|
+
banner: bannerImg ? bannerImg.url : null,
|
|
379
|
+
image: (bannerImg?.url || thumbImg?.url) || null,
|
|
299
380
|
};
|
|
300
|
-
if (thumbImg?.url) {
|
|
301
|
-
updatedMetadata.thumbnail = thumbImg.url;
|
|
302
|
-
updatedMetadata.image_url = thumbImg.url;
|
|
303
|
-
}
|
|
304
|
-
if (bannerImg?.url && bannerImg.url !== thumbImg?.url) {
|
|
305
|
-
updatedMetadata.banner = bannerImg.url;
|
|
306
|
-
updatedMetadata.image = bannerImg.url;
|
|
307
|
-
}
|
|
308
|
-
else if (thumbImg?.url) {
|
|
309
|
-
updatedMetadata.image = thumbImg.url;
|
|
310
|
-
}
|
|
311
381
|
await sdk_1.sdk.admin.productCategory.update(category.id, {
|
|
312
382
|
name: name.trim(),
|
|
313
383
|
handle: handle.trim() || undefined,
|
|
@@ -322,7 +392,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
322
392
|
name: name.trim(),
|
|
323
393
|
handle: handle.trim(),
|
|
324
394
|
description: description.trim() || null,
|
|
325
|
-
image_url:
|
|
395
|
+
image_url: thumbImg ? thumbImg.url : "",
|
|
326
396
|
rank: Number(rank) || 0,
|
|
327
397
|
sales_channels: selectedChannels,
|
|
328
398
|
is_active: isActive,
|
|
@@ -360,15 +430,15 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
360
430
|
};
|
|
361
431
|
return ((0, jsx_runtime_1.jsx)(ui_1.Drawer, { open: open, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(ui_1.Drawer.Content, { className: `transition-all duration-300 w-full z-50 ${isFullScreen
|
|
362
432
|
? "!max-w-full !w-screen !h-screen !inset-0 !rounded-none"
|
|
363
|
-
: "max-w-5xl xl:max-w-6xl w-[92vw]"}`, children: (0, jsx_runtime_1.jsxs)("
|
|
364
|
-
? "border-
|
|
365
|
-
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "General & Ranking" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("products"), className: `px-3 py-1.5 text-xs font-medium
|
|
366
|
-
? "border-
|
|
367
|
-
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Products Management" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("channels"), className: `px-3 py-1.5 text-xs font-medium
|
|
368
|
-
? "border-
|
|
369
|
-
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Storefronts & Media" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setActiveTab("youtube"), className: `px-3 py-1.5 text-xs font-medium
|
|
370
|
-
? "border-
|
|
371
|
-
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: ["YouTube Videos
|
|
433
|
+
: "max-w-5xl xl:max-w-6xl w-[92vw]"}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col h-full", children: [(0, jsx_runtime_1.jsxs)(ui_1.Drawer.Header, { className: "border-b border-ui-border-base pb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between w-full", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Drawer.Title, { className: "text-base font-semibold text-ui-fg-base", children: category.name }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Description, { className: "text-xs text-ui-fg-muted mt-0.5", children: ["Tier ", category.tier, " \u2022 ID: ", category.id] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setIsFullScreen((prev) => !prev), className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors cursor-pointer", title: isFullScreen ? "Exit Fullscreen" : "Expand to Full Page Modal", children: [(0, jsx_runtime_1.jsx)(icons_1.ArrowsPointingOut, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: isFullScreen ? "Exit Fullscreen" : "Full Page" })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => window.open(`/app/categories/${category.id}`, "_blank"), className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors cursor-pointer", title: "Open full category details page in a new tab", children: [(0, jsx_runtime_1.jsx)("span", { children: "Open Full Page" }), (0, jsx_runtime_1.jsx)(icons_1.ArrowUpRightMicro, { className: "w-3.5 h-3.5" })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 mt-3 border-b border-ui-border-base -mb-3", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("general"), className: `px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${activeTab === "general"
|
|
434
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
435
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "General & Ranking" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("products"), className: `px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${activeTab === "products"
|
|
436
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
437
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Products Management" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setActiveTab("channels"), className: `px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${activeTab === "channels"
|
|
438
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
439
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: "Storefronts & Media" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setActiveTab("youtube"), className: `flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${activeTab === "youtube"
|
|
440
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
441
|
+
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"}`, children: [(0, jsx_runtime_1.jsx)("span", { children: "YouTube Videos" }), youtubeUrls.length > 0 && ((0, jsx_runtime_1.jsx)("span", { className: "px-1.5 py-0.2 rounded-full text-[10px] font-bold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200", children: youtubeUrls.length }))] })] })] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Body, { className: "overflow-y-auto flex-1 p-6", children: [activeTab === "general" && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-name", className: "text-xs font-semibold", children: "Category Name *" }), (0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-name", value: name, onChange: (e) => setName(e.target.value), required: true })] }), (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", value: handle, onChange: (e) => setHandle(e.target.value), placeholder: "e.g. rayon-embroidery-thread" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 p-3 rounded-xl border border-ui-border-base bg-ui-bg-subtle/50", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-rank", className: "text-xs font-semibold", children: "Storefront Ranking (Sort Order)" }), (0, jsx_runtime_1.jsx)("span", { className: "text-[10px] font-mono text-ui-fg-muted", children: "Controls Storefront 2 & Monfil" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Input, { id: "edit-rank", type: "number", size: "small", value: rank, onChange: (e) => setRank(Number(e.target.value)), className: "w-24" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank((r) => Math.max(0, r - 1)), className: "px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer", title: "Move Earlier in Storefront", children: "\u2191 Earlier" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank((r) => r + 1), className: "px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer", title: "Move Later in Storefront", children: "\u2193 Later" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setRank(0), className: "px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer", title: "Set as First Category", children: "Top (#0)" })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-[11px] text-ui-fg-muted leading-relaxed", children: "Categories are sorted by rank ASC (0, 1, 2...). Lower numbers appear first on your storefront navigation menu." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { htmlFor: "edit-desc", className: "text-xs font-semibold", children: "Description" }), (0, jsx_runtime_1.jsx)(ui_1.Textarea, { id: "edit-desc", value: description, onChange: (e) => setDescription(e.target.value), rows: 3 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between pt-2 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Active Status" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Visible to shoppers on enabled storefronts" })] }), (0, jsx_runtime_1.jsx)(ui_1.Switch, { checked: isActive, onCheckedChange: setIsActive })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Internal Only" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Hidden from public storefront navigation" })] }), (0, jsx_runtime_1.jsx)(ui_1.Switch, { checked: isInternal, onCheckedChange: setIsInternal })] }), (0, jsx_runtime_1.jsx)("div", { className: "pt-3 border-t border-ui-border-base", children: (0, jsx_runtime_1.jsxs)(ui_1.Button, { type: "button", variant: "danger", size: "small", onClick: handleDelete, isLoading: isDeleting, className: "w-full flex items-center justify-center gap-1.5", children: [(0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Delete Category" })] }) })] })), activeTab === "products" && ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-3", children: (0, jsx_runtime_1.jsx)(category_products_manager_1.CategoryProductsManager, { categoryId: category.id, categoryName: category.name, initialMetadata: currentMetadata, onMetadataUpdate: (meta) => {
|
|
372
442
|
setCurrentMetadata(meta);
|
|
373
443
|
} }) })), activeTab === "channels" && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold", children: "Storefront Visibility" }), (0, jsx_runtime_1.jsx)(ui_1.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Designate which storefront sales channels display this category." }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2 mt-1", children: salesChannels.map((channel) => {
|
|
374
444
|
const isChecked = selectedChannels.includes(channel.id);
|
|
@@ -395,16 +465,16 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
395
465
|
e.preventDefault();
|
|
396
466
|
e.stopPropagation();
|
|
397
467
|
handleSetThumbnail(img);
|
|
398
|
-
}, className: "w-full py-0.5 bg-white text-neutral-900 rounded text-[9px] font-semibold hover:bg-neutral-100 transition-colors shadow-xs text-center", children: "Set Thumbnail" })), !isBanner && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
468
|
+
}, className: "w-full py-0.5 bg-white text-neutral-900 rounded text-[9px] font-semibold hover:bg-neutral-100 transition-colors shadow-xs text-center cursor-pointer", children: "Set Thumbnail" })), !isBanner && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
399
469
|
e.preventDefault();
|
|
400
470
|
e.stopPropagation();
|
|
401
471
|
handleSetBanner(img);
|
|
402
|
-
}, className: "w-full py-0.5 bg-emerald-
|
|
472
|
+
}, className: "w-full py-0.5 bg-emerald-600 text-white rounded text-[9px] font-semibold hover:bg-emerald-700 transition-colors shadow-xs text-center cursor-pointer", children: "Set as Banner" })), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
403
473
|
e.preventDefault();
|
|
404
474
|
e.stopPropagation();
|
|
405
475
|
handleDeleteImage(img);
|
|
406
|
-
}, className: "
|
|
407
|
-
}) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 mt-
|
|
476
|
+
}, className: "w-full py-0.5 bg-rose-600 text-white rounded text-[9px] font-semibold hover:bg-rose-700 transition-colors shadow-xs text-center cursor-pointer", children: "Delete" })] })] }, img.id || img.url || idx));
|
|
477
|
+
}) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 mt-2 p-3 rounded-xl border border-ui-border-base bg-ui-bg-subtle/30", children: [(0, jsx_runtime_1.jsx)(ui_1.Label, { className: "text-xs font-semibold text-ui-fg-base", children: "External Image URL (CDN / Manual)" }), (0, jsx_runtime_1.jsx)("p", { className: "text-[11px] text-ui-fg-muted", children: "Or paste a direct image URL (S3, Cloudinary, Imgur, Supabase) to link without uploading." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 mt-1", children: [(0, jsx_runtime_1.jsx)(ui_1.Input, { size: "small", type: "url", placeholder: "https://example.com/category-image.jpg", value: manualImageUrl, onChange: (e) => setManualImageUrl(e.target.value), className: "text-xs flex-1", onKeyDown: (e) => {
|
|
408
478
|
if (e.key === "Enter") {
|
|
409
479
|
e.preventDefault();
|
|
410
480
|
handleAddManualImage();
|
|
@@ -417,7 +487,7 @@ const EditCategoryDrawer = ({ open, onOpenChange, category, salesChannels, onSuc
|
|
|
417
487
|
} }), (0, jsx_runtime_1.jsxs)(ui_1.Button, { size: "small", type: "button", variant: "secondary", onClick: handleAddYoutube, className: "flex items-center gap-1 text-xs flex-shrink-0", children: [(0, jsx_runtime_1.jsx)(icons_1.Plus, { className: "w-3.5 h-3.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Add Video" })] })] }), youtubeUrls.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "p-8 text-center text-xs text-ui-fg-muted border border-dashed border-ui-border-base rounded-xl", children: "No demonstration videos attached yet." })) : ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 gap-3", children: youtubeUrls.map((url) => {
|
|
418
488
|
const id = extractYouTubeVideoId(url);
|
|
419
489
|
const thumb = id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : null;
|
|
420
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-subtle/40 shadow-xs", children: [(0, jsx_runtime_1.jsxs)("div", { className: "relative aspect-video bg-black/80 overflow-hidden flex items-center justify-center", children: [thumb ? ((0, jsx_runtime_1.jsx)("img", { src: thumb, alt: "YouTube Preview", className: "w-full h-full object-cover" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-xs text-white", children: "Video" })), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => handleRemoveYoutube(url), className: "absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors", title: "Remove video", children: (0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3 h-3" }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-2", children: (0, jsx_runtime_1.jsx)("span", { className: "text-[10px] text-ui-fg-muted truncate block font-mono", title: url, children: url }) })] }, url));
|
|
421
|
-
}) }))] }))] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Footer, { className: "flex justify-end gap-2 px-6 py-4 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsx)(ui_1.Button, { type: "button", variant: "secondary", size: "small", onClick: () => onOpenChange(false), disabled: isSubmitting || isDeleting, children: "Close" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { type: "
|
|
490
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col border border-ui-border-base rounded-xl overflow-hidden bg-ui-bg-subtle/40 shadow-xs", children: [(0, jsx_runtime_1.jsxs)("div", { className: "relative aspect-video bg-black/80 overflow-hidden flex items-center justify-center", children: [thumb ? ((0, jsx_runtime_1.jsx)("img", { src: thumb, alt: "YouTube Preview", className: "w-full h-full object-cover" })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-xs text-white", children: "Video" })), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => handleRemoveYoutube(url), className: "absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors cursor-pointer", title: "Remove video", children: (0, jsx_runtime_1.jsx)(icons_1.Trash, { className: "w-3 h-3" }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-2", children: (0, jsx_runtime_1.jsx)("span", { className: "text-[10px] text-ui-fg-muted truncate block font-mono", title: url, children: url }) })] }, url));
|
|
491
|
+
}) }))] }))] }), (0, jsx_runtime_1.jsxs)(ui_1.Drawer.Footer, { className: "flex justify-end gap-2 px-6 py-4 border-t border-ui-border-base", children: [(0, jsx_runtime_1.jsx)(ui_1.Button, { type: "button", variant: "secondary", size: "small", onClick: () => onOpenChange(false), disabled: isSubmitting || isDeleting, children: "Close" }), (0, jsx_runtime_1.jsx)(ui_1.Button, { type: "button", size: "small", onClick: () => handleSubmit(), isLoading: isSubmitting, disabled: isSubmitting || !name.trim() || isDeleting, children: "Save Category" })] })] }) }) }));
|
|
422
492
|
};
|
|
423
493
|
exports.EditCategoryDrawer = EditCategoryDrawer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hectasquare/medusa-plugin-category-tree",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "AAA Interactive Category Tree & Mind Map for Medusa v2 Admin with drag-and-drop reparenting, tier indicators, superscript ranks, multi-storefront filtering, and NotebookLM-style subtree collapse.",
|
|
5
5
|
"author": "HectaSquare Team",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MagnifyingGlass,
|
|
11
11
|
CheckMini,
|
|
12
12
|
Sparkles,
|
|
13
|
+
ArrowUpRightMicro,
|
|
13
14
|
} from "@medusajs/icons"
|
|
14
15
|
import { sdk } from "../../lib/sdk"
|
|
15
16
|
|
|
@@ -166,7 +167,7 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
166
167
|
await persistProductConfig(currentOrder, next)
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
// Remove products from category
|
|
170
|
+
// Remove products from category using official Medusa v2 JS SDK method
|
|
170
171
|
const handleRemoveProducts = async (idsToRemove: string[]) => {
|
|
171
172
|
if (idsToRemove.length === 0) return
|
|
172
173
|
const msg =
|
|
@@ -177,9 +178,8 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
177
178
|
|
|
178
179
|
setIsRemoving(true)
|
|
179
180
|
try {
|
|
180
|
-
await sdk.
|
|
181
|
-
|
|
182
|
-
body: { remove: idsToRemove },
|
|
181
|
+
await sdk.admin.productCategory.updateProducts(categoryId, {
|
|
182
|
+
remove: idsToRemove,
|
|
183
183
|
})
|
|
184
184
|
toast.success(`Removed ${idsToRemove.length} product(s) from category`)
|
|
185
185
|
setSelectedIds(new Set())
|
|
@@ -224,15 +224,15 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
224
224
|
}
|
|
225
225
|
}, [focusModalOpen, searchQuery, catalogPage, searchCatalog])
|
|
226
226
|
|
|
227
|
+
// Save added products using official Medusa v2 JS SDK method
|
|
227
228
|
const handleSaveAddedProducts = async () => {
|
|
228
229
|
const idsToAdd = Array.from(catalogSelectedIds)
|
|
229
230
|
if (idsToAdd.length === 0) return
|
|
230
231
|
|
|
231
232
|
setIsAdding(true)
|
|
232
233
|
try {
|
|
233
|
-
await sdk.
|
|
234
|
-
|
|
235
|
-
body: { add: idsToAdd },
|
|
234
|
+
await sdk.admin.productCategory.updateProducts(categoryId, {
|
|
235
|
+
add: idsToAdd,
|
|
236
236
|
})
|
|
237
237
|
toast.success(`Added ${idsToAdd.length} product(s) to ${categoryName}`)
|
|
238
238
|
setFocusModalOpen(false)
|
|
@@ -267,7 +267,11 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
267
267
|
<div className="flex items-center gap-1.5 p-1 rounded-lg bg-ui-bg-subtle border border-ui-border-base">
|
|
268
268
|
<button
|
|
269
269
|
type="button"
|
|
270
|
-
onClick={() =>
|
|
270
|
+
onClick={(e) => {
|
|
271
|
+
e.preventDefault()
|
|
272
|
+
e.stopPropagation()
|
|
273
|
+
setActiveView("all")
|
|
274
|
+
}}
|
|
271
275
|
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
272
276
|
activeView === "all"
|
|
273
277
|
? "bg-ui-bg-base text-ui-fg-base shadow-2xs font-semibold"
|
|
@@ -278,7 +282,11 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
278
282
|
</button>
|
|
279
283
|
<button
|
|
280
284
|
type="button"
|
|
281
|
-
onClick={() =>
|
|
285
|
+
onClick={(e) => {
|
|
286
|
+
e.preventDefault()
|
|
287
|
+
e.stopPropagation()
|
|
288
|
+
setActiveView("top_sellers")
|
|
289
|
+
}}
|
|
282
290
|
className={`flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
283
291
|
activeView === "top_sellers"
|
|
284
292
|
? "bg-amber-500/10 text-amber-700 dark:text-amber-400 font-semibold"
|
|
@@ -293,10 +301,15 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
293
301
|
<div className="flex items-center gap-2">
|
|
294
302
|
{selectedIds.size > 0 && (
|
|
295
303
|
<Button
|
|
304
|
+
type="button"
|
|
296
305
|
size="small"
|
|
297
306
|
variant="danger"
|
|
298
307
|
disabled={isRemoving}
|
|
299
|
-
onClick={() =>
|
|
308
|
+
onClick={(e) => {
|
|
309
|
+
e.preventDefault()
|
|
310
|
+
e.stopPropagation()
|
|
311
|
+
handleRemoveProducts(Array.from(selectedIds))
|
|
312
|
+
}}
|
|
300
313
|
className="flex items-center gap-1 text-xs"
|
|
301
314
|
>
|
|
302
315
|
<Trash className="w-3.5 h-3.5" />
|
|
@@ -305,9 +318,12 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
305
318
|
)}
|
|
306
319
|
|
|
307
320
|
<Button
|
|
321
|
+
type="button"
|
|
308
322
|
size="small"
|
|
309
323
|
variant="secondary"
|
|
310
|
-
onClick={() => {
|
|
324
|
+
onClick={(e) => {
|
|
325
|
+
e.preventDefault()
|
|
326
|
+
e.stopPropagation()
|
|
311
327
|
setCatalogSelectedIds(new Set())
|
|
312
328
|
setSearchQuery("")
|
|
313
329
|
setCatalogPage(0)
|
|
@@ -336,9 +352,17 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
336
352
|
</p>
|
|
337
353
|
{activeView === "all" && (
|
|
338
354
|
<Button
|
|
355
|
+
type="button"
|
|
339
356
|
size="small"
|
|
340
357
|
variant="secondary"
|
|
341
|
-
onClick={() =>
|
|
358
|
+
onClick={(e) => {
|
|
359
|
+
e.preventDefault()
|
|
360
|
+
e.stopPropagation()
|
|
361
|
+
setCatalogSelectedIds(new Set())
|
|
362
|
+
setSearchQuery("")
|
|
363
|
+
setCatalogPage(0)
|
|
364
|
+
setFocusModalOpen(true)
|
|
365
|
+
}}
|
|
342
366
|
className="flex items-center gap-1 text-xs"
|
|
343
367
|
>
|
|
344
368
|
<Plus className="w-3.5 h-3.5" />
|
|
@@ -435,20 +459,33 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
435
459
|
</div>
|
|
436
460
|
</td>
|
|
437
461
|
|
|
438
|
-
{/* Product Thumbnail & Title */}
|
|
462
|
+
{/* Product Thumbnail & Title (Clickable link to Medusa Admin product page) */}
|
|
439
463
|
<td className="py-2.5 px-3">
|
|
440
464
|
<div className="flex items-center gap-2.5 min-w-0">
|
|
441
|
-
<
|
|
465
|
+
<a
|
|
466
|
+
href={`/app/products/${p.id}`}
|
|
467
|
+
target="_blank"
|
|
468
|
+
rel="noopener noreferrer"
|
|
469
|
+
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 hover:opacity-80 transition-opacity"
|
|
470
|
+
title={`Open ${p.title} in new tab`}
|
|
471
|
+
>
|
|
442
472
|
{p.thumbnail ? (
|
|
443
473
|
<img src={p.thumbnail} alt={p.title} className="w-full h-full object-cover" />
|
|
444
474
|
) : (
|
|
445
475
|
<span className="text-[9px] text-ui-fg-muted">IMG</span>
|
|
446
476
|
)}
|
|
447
|
-
</
|
|
477
|
+
</a>
|
|
448
478
|
<div className="flex flex-col min-w-0">
|
|
449
|
-
<
|
|
450
|
-
{p.
|
|
451
|
-
|
|
479
|
+
<a
|
|
480
|
+
href={`/app/products/${p.id}`}
|
|
481
|
+
target="_blank"
|
|
482
|
+
rel="noopener noreferrer"
|
|
483
|
+
className="font-semibold text-ui-fg-base hover:text-ui-fg-interactive truncate leading-snug hover:underline flex items-center gap-1 group/pname"
|
|
484
|
+
title={`Open ${p.title} in new tab`}
|
|
485
|
+
>
|
|
486
|
+
<span className="truncate">{p.title}</span>
|
|
487
|
+
<ArrowUpRightMicro className="w-3.5 h-3.5 text-ui-fg-muted group-hover/pname:text-ui-fg-interactive transition-colors shrink-0" />
|
|
488
|
+
</a>
|
|
452
489
|
</div>
|
|
453
490
|
</div>
|
|
454
491
|
</td>
|
|
@@ -462,7 +499,7 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
462
499
|
e.stopPropagation()
|
|
463
500
|
handleToggleTopSeller(p.id, p.title)
|
|
464
501
|
}}
|
|
465
|
-
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border transition-all ${
|
|
502
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold border transition-all cursor-pointer ${
|
|
466
503
|
isTopSeller
|
|
467
504
|
? "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-300 dark:border-amber-700 shadow-2xs"
|
|
468
505
|
: "bg-ui-bg-subtle text-ui-fg-muted border-ui-border-base hover:text-amber-600 hover:border-amber-300"
|
|
@@ -470,7 +507,7 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
470
507
|
title={
|
|
471
508
|
isTopSeller
|
|
472
509
|
? "Click to remove from Top Commercial Sellers"
|
|
473
|
-
: "Click to mark as Top Commercial Seller (Reflected
|
|
510
|
+
: "Click to mark as Top Commercial Seller (Reflected on Storefront 2)"
|
|
474
511
|
}
|
|
475
512
|
>
|
|
476
513
|
<span>{isTopSeller ? "★" : "☆"}</span>
|
|
@@ -507,7 +544,7 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
507
544
|
e.stopPropagation()
|
|
508
545
|
handleRemoveProducts([p.id])
|
|
509
546
|
}}
|
|
510
|
-
className="p-1 text-ui-fg-muted hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded transition-colors"
|
|
547
|
+
className="p-1 text-ui-fg-muted hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded transition-colors cursor-pointer"
|
|
511
548
|
>
|
|
512
549
|
<Trash className="w-3.5 h-3.5" />
|
|
513
550
|
</button>
|
|
@@ -530,8 +567,12 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
530
567
|
<div className="flex items-center gap-3">
|
|
531
568
|
<button
|
|
532
569
|
type="button"
|
|
533
|
-
onClick={() =>
|
|
534
|
-
|
|
570
|
+
onClick={(e) => {
|
|
571
|
+
e.preventDefault()
|
|
572
|
+
e.stopPropagation()
|
|
573
|
+
setFocusModalOpen(false)
|
|
574
|
+
}}
|
|
575
|
+
className="p-1.5 rounded-lg text-ui-fg-muted hover:text-ui-fg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer"
|
|
535
576
|
title="Close (ESC)"
|
|
536
577
|
>
|
|
537
578
|
<span className="text-sm font-bold">✕</span>
|
|
@@ -728,16 +769,24 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
728
769
|
<button
|
|
729
770
|
type="button"
|
|
730
771
|
disabled={catalogPage === 0}
|
|
731
|
-
onClick={() =>
|
|
732
|
-
|
|
772
|
+
onClick={(e) => {
|
|
773
|
+
e.preventDefault()
|
|
774
|
+
e.stopPropagation()
|
|
775
|
+
setCatalogPage((p) => Math.max(0, p - 1))
|
|
776
|
+
}}
|
|
777
|
+
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 cursor-pointer"
|
|
733
778
|
>
|
|
734
779
|
Prev
|
|
735
780
|
</button>
|
|
736
781
|
<button
|
|
737
782
|
type="button"
|
|
738
783
|
disabled={(catalogPage + 1) * PAGE_SIZE >= catalogTotalCount}
|
|
739
|
-
onClick={() =>
|
|
740
|
-
|
|
784
|
+
onClick={(e) => {
|
|
785
|
+
e.preventDefault()
|
|
786
|
+
e.stopPropagation()
|
|
787
|
+
setCatalogPage((p) => p + 1)
|
|
788
|
+
}}
|
|
789
|
+
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 cursor-pointer"
|
|
741
790
|
>
|
|
742
791
|
Next
|
|
743
792
|
</button>
|
|
@@ -745,17 +794,27 @@ export const CategoryProductsManager: React.FC<CategoryProductsManagerProps> = (
|
|
|
745
794
|
<div className="h-4 w-[1px] bg-ui-border-base mx-1" />
|
|
746
795
|
|
|
747
796
|
<Button
|
|
797
|
+
type="button"
|
|
748
798
|
size="small"
|
|
749
799
|
variant="secondary"
|
|
750
|
-
onClick={() =>
|
|
800
|
+
onClick={(e) => {
|
|
801
|
+
e.preventDefault()
|
|
802
|
+
e.stopPropagation()
|
|
803
|
+
setFocusModalOpen(false)
|
|
804
|
+
}}
|
|
751
805
|
>
|
|
752
806
|
Cancel
|
|
753
807
|
</Button>
|
|
754
808
|
<Button
|
|
809
|
+
type="button"
|
|
755
810
|
size="small"
|
|
756
811
|
variant="primary"
|
|
757
812
|
disabled={catalogSelectedIds.size === 0 || isAdding}
|
|
758
|
-
onClick={
|
|
813
|
+
onClick={(e) => {
|
|
814
|
+
e.preventDefault()
|
|
815
|
+
e.stopPropagation()
|
|
816
|
+
handleSaveAddedProducts()
|
|
817
|
+
}}
|
|
759
818
|
className="flex items-center gap-1 text-xs min-w-[70px] justify-center"
|
|
760
819
|
>
|
|
761
820
|
<CheckMini className="w-3.5 h-3.5" />
|
|
@@ -122,8 +122,13 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
122
122
|
// Endpoint fallback
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
//
|
|
126
|
-
if (metadata
|
|
125
|
+
// Explicit metadata.images check: if metadata.images array exists (even if empty)
|
|
126
|
+
if (metadata && Array.isArray(metadata.images)) {
|
|
127
|
+
if (metadata.images.length === 0) {
|
|
128
|
+
setCategoryImages([])
|
|
129
|
+
setImageUrl("")
|
|
130
|
+
return
|
|
131
|
+
}
|
|
127
132
|
setCategoryImages(metadata.images)
|
|
128
133
|
const thumb = metadata.images.find((img: any) => img.type === "thumbnail") || metadata.images[0]
|
|
129
134
|
if (thumb?.url) setImageUrl(thumb.url)
|
|
@@ -132,7 +137,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
132
137
|
|
|
133
138
|
// Legacy fallback
|
|
134
139
|
const list: Array<{ url: string; type: string; id?: string }> = []
|
|
135
|
-
const thumb =
|
|
140
|
+
const thumb = metadata?.thumbnail || metadata?.image_url || currentImageUrl
|
|
136
141
|
if (thumb) {
|
|
137
142
|
list.push({ url: thumb, type: "thumbnail" })
|
|
138
143
|
}
|
|
@@ -267,8 +272,33 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
267
272
|
)
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
// Delete category image
|
|
275
|
+
// Delete category image with immediate persistence & explicit nulls
|
|
271
276
|
const handleDeleteImage = async (img: { id?: string; url: string }) => {
|
|
277
|
+
const remaining = categoryImages.filter(
|
|
278
|
+
(i) => i.url !== img.url && (img.id ? i.id !== img.id : true)
|
|
279
|
+
)
|
|
280
|
+
const nextThumb =
|
|
281
|
+
remaining.find((i) => i.type === "thumbnail") || remaining[0] || null
|
|
282
|
+
const nextBanner =
|
|
283
|
+
remaining.find((i) => i.type === "banner") ||
|
|
284
|
+
(remaining.length > 1 ? remaining.find((i) => i.url !== nextThumb?.url) : null) ||
|
|
285
|
+
null
|
|
286
|
+
|
|
287
|
+
// Update local state immediately
|
|
288
|
+
setCategoryImages(remaining)
|
|
289
|
+
setImageUrl(nextThumb ? nextThumb.url : "")
|
|
290
|
+
|
|
291
|
+
const updatedMeta: Record<string, any> = {
|
|
292
|
+
...(currentMetadata || {}),
|
|
293
|
+
images: remaining,
|
|
294
|
+
thumbnail: nextThumb ? nextThumb.url : null,
|
|
295
|
+
image_url: nextThumb ? nextThumb.url : null,
|
|
296
|
+
banner: nextBanner ? nextBanner.url : null,
|
|
297
|
+
image: (nextBanner?.url || nextThumb?.url) || null,
|
|
298
|
+
}
|
|
299
|
+
setCurrentMetadata(updatedMeta)
|
|
300
|
+
|
|
301
|
+
// Call backend endpoint if DB relation exists
|
|
272
302
|
if (img.id) {
|
|
273
303
|
try {
|
|
274
304
|
await sdk.client.fetch(`/admin/categories/${category.id}/images/batch`, {
|
|
@@ -278,20 +308,27 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
278
308
|
ids: [img.id],
|
|
279
309
|
},
|
|
280
310
|
})
|
|
281
|
-
} catch {
|
|
282
|
-
|
|
311
|
+
} catch (err: any) {
|
|
312
|
+
console.warn("Could not delete from backend images endpoint:", err)
|
|
283
313
|
}
|
|
284
314
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
|
|
316
|
+
// Immediately persist metadata changes to Medusa
|
|
317
|
+
try {
|
|
318
|
+
await sdk.admin.productCategory.update(category.id, {
|
|
319
|
+
metadata: updatedMeta,
|
|
320
|
+
})
|
|
321
|
+
const updatedCat: CategoryTreeNode = {
|
|
322
|
+
...category,
|
|
323
|
+
image_url: nextThumb ? nextThumb.url : "",
|
|
324
|
+
metadata: updatedMeta,
|
|
325
|
+
}
|
|
326
|
+
onSuccess(updatedCat)
|
|
327
|
+
toast.success("Image removed and changes saved")
|
|
328
|
+
} catch (err: any) {
|
|
329
|
+
console.warn("Could not immediately update category metadata on delete:", err)
|
|
330
|
+
toast.info("Image removed from gallery (remember to save)")
|
|
293
331
|
}
|
|
294
|
-
toast.info("Image removed")
|
|
295
332
|
}
|
|
296
333
|
|
|
297
334
|
if (!category) return null
|
|
@@ -304,7 +341,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
304
341
|
)
|
|
305
342
|
}
|
|
306
343
|
|
|
307
|
-
const handleAddYoutube = () => {
|
|
344
|
+
const handleAddYoutube = async () => {
|
|
308
345
|
const raw = newYoutubeInput.trim()
|
|
309
346
|
if (!raw) return
|
|
310
347
|
const id = extractYouTubeVideoId(raw)
|
|
@@ -317,17 +354,57 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
317
354
|
toast.error("Video already added")
|
|
318
355
|
return
|
|
319
356
|
}
|
|
320
|
-
|
|
357
|
+
const nextUrls = [...youtubeUrls, cleanUrl]
|
|
358
|
+
setYoutubeUrls(nextUrls)
|
|
321
359
|
setNewYoutubeInput("")
|
|
322
|
-
|
|
360
|
+
|
|
361
|
+
const updatedMeta = {
|
|
362
|
+
...(currentMetadata || {}),
|
|
363
|
+
youtube_urls: nextUrls,
|
|
364
|
+
youtube_url: nextUrls[0] || null,
|
|
365
|
+
}
|
|
366
|
+
setCurrentMetadata(updatedMeta)
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
await sdk.admin.productCategory.update(category.id, {
|
|
370
|
+
metadata: updatedMeta,
|
|
371
|
+
})
|
|
372
|
+
onSuccess({
|
|
373
|
+
...category,
|
|
374
|
+
metadata: updatedMeta,
|
|
375
|
+
})
|
|
376
|
+
toast.success("YouTube video added and saved")
|
|
377
|
+
} catch {
|
|
378
|
+
toast.success("YouTube video added")
|
|
379
|
+
}
|
|
323
380
|
}
|
|
324
381
|
|
|
325
|
-
const handleRemoveYoutube = (urlToRemove: string) => {
|
|
326
|
-
|
|
382
|
+
const handleRemoveYoutube = async (urlToRemove: string) => {
|
|
383
|
+
const nextUrls = youtubeUrls.filter((u) => u !== urlToRemove)
|
|
384
|
+
setYoutubeUrls(nextUrls)
|
|
385
|
+
const updatedMeta = {
|
|
386
|
+
...(currentMetadata || {}),
|
|
387
|
+
youtube_urls: nextUrls,
|
|
388
|
+
youtube_url: nextUrls[0] || null,
|
|
389
|
+
}
|
|
390
|
+
setCurrentMetadata(updatedMeta)
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await sdk.admin.productCategory.update(category.id, {
|
|
394
|
+
metadata: updatedMeta,
|
|
395
|
+
})
|
|
396
|
+
onSuccess({
|
|
397
|
+
...category,
|
|
398
|
+
metadata: updatedMeta,
|
|
399
|
+
})
|
|
400
|
+
toast.success("Video removed and changes saved")
|
|
401
|
+
} catch {
|
|
402
|
+
toast.info("Video removed (remember to save)")
|
|
403
|
+
}
|
|
327
404
|
}
|
|
328
405
|
|
|
329
|
-
const handleSubmit = async (e
|
|
330
|
-
e.preventDefault()
|
|
406
|
+
const handleSubmit = async (e?: React.FormEvent) => {
|
|
407
|
+
if (e && e.preventDefault) e.preventDefault()
|
|
331
408
|
if (!name.trim()) {
|
|
332
409
|
toast.error("Category name is required")
|
|
333
410
|
return
|
|
@@ -337,26 +414,23 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
337
414
|
try {
|
|
338
415
|
const thumbImg =
|
|
339
416
|
categoryImages.find((img) => img.type === "thumbnail") ||
|
|
340
|
-
categoryImages[0]
|
|
417
|
+
categoryImages[0] ||
|
|
418
|
+
null
|
|
341
419
|
const bannerImg =
|
|
342
420
|
categoryImages.find((img) => img.type === "banner") ||
|
|
343
|
-
categoryImages.find((img) => img.url !== thumbImg?.url)
|
|
421
|
+
(categoryImages.length > 1 ? categoryImages.find((img) => img.url !== thumbImg?.url) : null) ||
|
|
422
|
+
null
|
|
344
423
|
|
|
345
424
|
const updatedMetadata: Record<string, any> = {
|
|
346
425
|
...(currentMetadata || {}),
|
|
347
426
|
sales_channels: selectedChannels,
|
|
348
427
|
youtube_urls: youtubeUrls,
|
|
428
|
+
youtube_url: youtubeUrls[0] || null,
|
|
349
429
|
images: categoryImages,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
if (bannerImg?.url && bannerImg.url !== thumbImg?.url) {
|
|
356
|
-
updatedMetadata.banner = bannerImg.url
|
|
357
|
-
updatedMetadata.image = bannerImg.url
|
|
358
|
-
} else if (thumbImg?.url) {
|
|
359
|
-
updatedMetadata.image = thumbImg.url
|
|
430
|
+
thumbnail: thumbImg ? thumbImg.url : null,
|
|
431
|
+
image_url: thumbImg ? thumbImg.url : null,
|
|
432
|
+
banner: bannerImg ? bannerImg.url : null,
|
|
433
|
+
image: (bannerImg?.url || thumbImg?.url) || null,
|
|
360
434
|
}
|
|
361
435
|
|
|
362
436
|
await sdk.admin.productCategory.update(category.id, {
|
|
@@ -374,7 +448,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
374
448
|
name: name.trim(),
|
|
375
449
|
handle: handle.trim(),
|
|
376
450
|
description: description.trim() || null,
|
|
377
|
-
image_url:
|
|
451
|
+
image_url: thumbImg ? thumbImg.url : "",
|
|
378
452
|
rank: Number(rank) || 0,
|
|
379
453
|
sales_channels: selectedChannels,
|
|
380
454
|
is_active: isActive,
|
|
@@ -419,7 +493,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
419
493
|
: "max-w-5xl xl:max-w-6xl w-[92vw]"
|
|
420
494
|
}`}
|
|
421
495
|
>
|
|
422
|
-
<
|
|
496
|
+
<div className="flex flex-col h-full">
|
|
423
497
|
<Drawer.Header className="border-b border-ui-border-base pb-3">
|
|
424
498
|
<div className="flex items-center justify-between w-full">
|
|
425
499
|
<div>
|
|
@@ -436,7 +510,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
436
510
|
<button
|
|
437
511
|
type="button"
|
|
438
512
|
onClick={() => setIsFullScreen((prev) => !prev)}
|
|
439
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors"
|
|
513
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors cursor-pointer"
|
|
440
514
|
title={isFullScreen ? "Exit Fullscreen" : "Expand to Full Page Modal"}
|
|
441
515
|
>
|
|
442
516
|
<ArrowsPointingOut className="w-3.5 h-3.5" />
|
|
@@ -447,7 +521,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
447
521
|
<button
|
|
448
522
|
type="button"
|
|
449
523
|
onClick={() => window.open(`/app/categories/${category.id}`, "_blank")}
|
|
450
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors"
|
|
524
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-ui-fg-subtle hover:text-ui-fg-base rounded-lg border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-base shadow-2xs transition-colors cursor-pointer"
|
|
451
525
|
title="Open full category details page in a new tab"
|
|
452
526
|
>
|
|
453
527
|
<span>Open Full Page</span>
|
|
@@ -456,59 +530,61 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
456
530
|
</div>
|
|
457
531
|
</div>
|
|
458
532
|
|
|
459
|
-
{/*
|
|
460
|
-
<div className="flex items-center gap-1 mt-3 border-b border-ui-border-base
|
|
533
|
+
{/* Sub-tab Navigation */}
|
|
534
|
+
<div className="flex items-center gap-1 mt-3 border-b border-ui-border-base -mb-3">
|
|
461
535
|
<button
|
|
462
536
|
type="button"
|
|
463
537
|
onClick={() => setActiveTab("general")}
|
|
464
|
-
className={`px-3 py-1.5 text-xs font-medium
|
|
538
|
+
className={`px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
465
539
|
activeTab === "general"
|
|
466
|
-
? "border-
|
|
540
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
467
541
|
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
|
|
468
542
|
}`}
|
|
469
543
|
>
|
|
470
544
|
General & Ranking
|
|
471
545
|
</button>
|
|
472
|
-
|
|
473
546
|
<button
|
|
474
547
|
type="button"
|
|
475
548
|
onClick={() => setActiveTab("products")}
|
|
476
|
-
className={`px-3 py-1.5 text-xs font-medium
|
|
549
|
+
className={`px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
477
550
|
activeTab === "products"
|
|
478
|
-
? "border-
|
|
551
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
479
552
|
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
|
|
480
553
|
}`}
|
|
481
554
|
>
|
|
482
555
|
Products Management
|
|
483
556
|
</button>
|
|
484
|
-
|
|
485
557
|
<button
|
|
486
558
|
type="button"
|
|
487
559
|
onClick={() => setActiveTab("channels")}
|
|
488
|
-
className={`px-3 py-1.5 text-xs font-medium
|
|
560
|
+
className={`px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
489
561
|
activeTab === "channels"
|
|
490
|
-
? "border-
|
|
562
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
491
563
|
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
|
|
492
564
|
}`}
|
|
493
565
|
>
|
|
494
566
|
Storefronts & Media
|
|
495
567
|
</button>
|
|
496
|
-
|
|
497
568
|
<button
|
|
498
569
|
type="button"
|
|
499
570
|
onClick={() => setActiveTab("youtube")}
|
|
500
|
-
className={`px-3 py-1.5 text-xs font-medium
|
|
571
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors cursor-pointer ${
|
|
501
572
|
activeTab === "youtube"
|
|
502
|
-
? "border-
|
|
573
|
+
? "border-blue-600 text-blue-600 font-semibold"
|
|
503
574
|
: "border-transparent text-ui-fg-muted hover:text-ui-fg-base"
|
|
504
575
|
}`}
|
|
505
576
|
>
|
|
506
|
-
YouTube Videos
|
|
577
|
+
<span>YouTube Videos</span>
|
|
578
|
+
{youtubeUrls.length > 0 && (
|
|
579
|
+
<span className="px-1.5 py-0.2 rounded-full text-[10px] font-bold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
|
580
|
+
{youtubeUrls.length}
|
|
581
|
+
</span>
|
|
582
|
+
)}
|
|
507
583
|
</button>
|
|
508
584
|
</div>
|
|
509
585
|
</Drawer.Header>
|
|
510
586
|
|
|
511
|
-
<Drawer.Body className="
|
|
587
|
+
<Drawer.Body className="overflow-y-auto flex-1 p-6">
|
|
512
588
|
{/* TAB 1: GENERAL & RANKING */}
|
|
513
589
|
{activeTab === "general" && (
|
|
514
590
|
<div className="flex flex-col gap-4">
|
|
@@ -518,25 +594,26 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
518
594
|
</Label>
|
|
519
595
|
<Input
|
|
520
596
|
id="edit-name"
|
|
521
|
-
size="small"
|
|
522
597
|
value={name}
|
|
523
598
|
onChange={(e) => setName(e.target.value)}
|
|
599
|
+
required
|
|
524
600
|
/>
|
|
525
601
|
</div>
|
|
526
602
|
|
|
527
603
|
<div className="flex flex-col gap-1.5">
|
|
528
604
|
<Label htmlFor="edit-handle" className="text-xs font-semibold">
|
|
529
|
-
Handle (URL
|
|
605
|
+
Handle (URL Slug)
|
|
530
606
|
</Label>
|
|
531
607
|
<Input
|
|
532
608
|
id="edit-handle"
|
|
533
|
-
size="small"
|
|
534
609
|
value={handle}
|
|
535
610
|
onChange={(e) => setHandle(e.target.value)}
|
|
611
|
+
placeholder="e.g. rayon-embroidery-thread"
|
|
536
612
|
/>
|
|
537
613
|
</div>
|
|
538
614
|
|
|
539
|
-
|
|
615
|
+
{/* Direct Rank Modifier */}
|
|
616
|
+
<div className="flex flex-col gap-1.5 p-3 rounded-xl border border-ui-border-base bg-ui-bg-subtle/50">
|
|
540
617
|
<div className="flex items-center justify-between">
|
|
541
618
|
<Label htmlFor="edit-rank" className="text-xs font-semibold">
|
|
542
619
|
Storefront Ranking (Sort Order)
|
|
@@ -558,7 +635,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
558
635
|
<button
|
|
559
636
|
type="button"
|
|
560
637
|
onClick={() => setRank((r) => Math.max(0, r - 1))}
|
|
561
|
-
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
638
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer"
|
|
562
639
|
title="Move Earlier in Storefront"
|
|
563
640
|
>
|
|
564
641
|
↑ Earlier
|
|
@@ -566,7 +643,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
566
643
|
<button
|
|
567
644
|
type="button"
|
|
568
645
|
onClick={() => setRank((r) => r + 1)}
|
|
569
|
-
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
646
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer"
|
|
570
647
|
title="Move Later in Storefront"
|
|
571
648
|
>
|
|
572
649
|
↓ Later
|
|
@@ -574,7 +651,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
574
651
|
<button
|
|
575
652
|
type="button"
|
|
576
653
|
onClick={() => setRank(0)}
|
|
577
|
-
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors"
|
|
654
|
+
className="px-2 py-1 text-xs font-medium rounded border border-ui-border-base bg-ui-bg-base hover:bg-ui-bg-subtle transition-colors cursor-pointer"
|
|
578
655
|
title="Set as First Category"
|
|
579
656
|
>
|
|
580
657
|
Top (#0)
|
|
@@ -786,7 +863,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
786
863
|
e.stopPropagation()
|
|
787
864
|
handleSetThumbnail(img)
|
|
788
865
|
}}
|
|
789
|
-
className="w-full py-0.5 bg-white text-neutral-900 rounded text-[9px] font-semibold hover:bg-neutral-100 transition-colors shadow-xs text-center"
|
|
866
|
+
className="w-full py-0.5 bg-white text-neutral-900 rounded text-[9px] font-semibold hover:bg-neutral-100 transition-colors shadow-xs text-center cursor-pointer"
|
|
790
867
|
>
|
|
791
868
|
Set Thumbnail
|
|
792
869
|
</button>
|
|
@@ -799,7 +876,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
799
876
|
e.stopPropagation()
|
|
800
877
|
handleSetBanner(img)
|
|
801
878
|
}}
|
|
802
|
-
className="w-full py-0.5 bg-emerald-
|
|
879
|
+
className="w-full py-0.5 bg-emerald-600 text-white rounded text-[9px] font-semibold hover:bg-emerald-700 transition-colors shadow-xs text-center cursor-pointer"
|
|
803
880
|
>
|
|
804
881
|
Set as Banner
|
|
805
882
|
</button>
|
|
@@ -811,10 +888,9 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
811
888
|
e.stopPropagation()
|
|
812
889
|
handleDeleteImage(img)
|
|
813
890
|
}}
|
|
814
|
-
className="
|
|
815
|
-
title="Remove image"
|
|
891
|
+
className="w-full py-0.5 bg-rose-600 text-white rounded text-[9px] font-semibold hover:bg-rose-700 transition-colors shadow-xs text-center cursor-pointer"
|
|
816
892
|
>
|
|
817
|
-
|
|
893
|
+
Delete
|
|
818
894
|
</button>
|
|
819
895
|
</div>
|
|
820
896
|
</div>
|
|
@@ -824,19 +900,22 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
824
900
|
</div>
|
|
825
901
|
)}
|
|
826
902
|
|
|
827
|
-
{/* Manual
|
|
828
|
-
<div className="flex flex-col gap-1.5 mt-
|
|
829
|
-
<Label
|
|
830
|
-
|
|
903
|
+
{/* Manual / External Image URL Entry */}
|
|
904
|
+
<div className="flex flex-col gap-1.5 mt-2 p-3 rounded-xl border border-ui-border-base bg-ui-bg-subtle/30">
|
|
905
|
+
<Label className="text-xs font-semibold text-ui-fg-base">
|
|
906
|
+
External Image URL (CDN / Manual)
|
|
831
907
|
</Label>
|
|
832
|
-
<
|
|
908
|
+
<p className="text-[11px] text-ui-fg-muted">
|
|
909
|
+
Or paste a direct image URL (S3, Cloudinary, Imgur, Supabase) to link without uploading.
|
|
910
|
+
</p>
|
|
911
|
+
<div className="flex items-center gap-2 mt-1">
|
|
833
912
|
<Input
|
|
834
|
-
id="manual-image-url"
|
|
835
913
|
size="small"
|
|
914
|
+
type="url"
|
|
915
|
+
placeholder="https://example.com/category-image.jpg"
|
|
836
916
|
value={manualImageUrl}
|
|
837
917
|
onChange={(e) => setManualImageUrl(e.target.value)}
|
|
838
|
-
|
|
839
|
-
className="flex-1 text-xs"
|
|
918
|
+
className="text-xs flex-1"
|
|
840
919
|
onKeyDown={(e) => {
|
|
841
920
|
if (e.key === "Enter") {
|
|
842
921
|
e.preventDefault()
|
|
@@ -922,7 +1001,7 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
922
1001
|
<button
|
|
923
1002
|
type="button"
|
|
924
1003
|
onClick={() => handleRemoveYoutube(url)}
|
|
925
|
-
className="absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors"
|
|
1004
|
+
className="absolute top-1.5 right-1.5 p-1 bg-black/70 hover:bg-rose-600 text-white rounded-full transition-colors cursor-pointer"
|
|
926
1005
|
title="Remove video"
|
|
927
1006
|
>
|
|
928
1007
|
<Trash className="w-3 h-3" />
|
|
@@ -953,15 +1032,16 @@ export const EditCategoryDrawer: React.FC<EditCategoryDrawerProps> = ({
|
|
|
953
1032
|
Close
|
|
954
1033
|
</Button>
|
|
955
1034
|
<Button
|
|
956
|
-
type="
|
|
1035
|
+
type="button"
|
|
957
1036
|
size="small"
|
|
1037
|
+
onClick={() => handleSubmit()}
|
|
958
1038
|
isLoading={isSubmitting}
|
|
959
1039
|
disabled={isSubmitting || !name.trim() || isDeleting}
|
|
960
1040
|
>
|
|
961
1041
|
Save Category
|
|
962
1042
|
</Button>
|
|
963
1043
|
</Drawer.Footer>
|
|
964
|
-
</
|
|
1044
|
+
</div>
|
|
965
1045
|
</Drawer.Content>
|
|
966
1046
|
</Drawer>
|
|
967
1047
|
)
|