@lodashventure/medusa-product-content 1.2.20 → 1.2.21
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/index.d.ts +1 -0
- package/.medusa/server/index.js +7 -0
- package/.medusa/server/modules/product-content/index.d.ts +21 -0
- package/.medusa/server/modules/product-content/index.js +13 -0
- package/.medusa/server/modules/product-content/models/product-content.d.ts +7 -0
- package/.medusa/server/modules/product-content/models/product-content.js +19 -0
- package/.medusa/server/modules/product-content/service.d.ts +12 -0
- package/.medusa/server/modules/product-content/service.js +10 -0
- package/.medusa/server/src/admin/index.js +1597 -0
- package/.medusa/server/src/admin/index.mjs +1595 -0
- package/.medusa/server/types/index.d.ts +173 -0
- package/.medusa/server/types/index.js +6 -0
- package/package.json +8 -7
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
3
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
4
|
+
import { Toaster, Heading, Text, Button, IconButton, Badge, Input, Label, Switch, toast, Container, Alert, Tabs, Textarea, Table } from "@medusajs/ui";
|
|
5
|
+
import { Plus, ChevronUpDown, XMark, PencilSquare, EllipsisHorizontal, Trash, ArrowUturnLeft, ArrowDownTray, DocumentText, SquareTwoStack, MagnifyingGlass } from "@medusajs/icons";
|
|
6
|
+
import Medusa from "@medusajs/js-sdk";
|
|
7
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
8
|
+
import "@medusajs/admin-shared";
|
|
9
|
+
const sdk = new Medusa({
|
|
10
|
+
baseUrl: "/",
|
|
11
|
+
debug: false,
|
|
12
|
+
auth: {
|
|
13
|
+
type: "session"
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const DEFAULT_ALLOWED_TAGS = [
|
|
17
|
+
"h1",
|
|
18
|
+
"h2",
|
|
19
|
+
"h3",
|
|
20
|
+
"h4",
|
|
21
|
+
"h5",
|
|
22
|
+
"h6",
|
|
23
|
+
"p",
|
|
24
|
+
"br",
|
|
25
|
+
"hr",
|
|
26
|
+
"strong",
|
|
27
|
+
"b",
|
|
28
|
+
"em",
|
|
29
|
+
"i",
|
|
30
|
+
"u",
|
|
31
|
+
"strike",
|
|
32
|
+
"s",
|
|
33
|
+
"del",
|
|
34
|
+
"ul",
|
|
35
|
+
"ol",
|
|
36
|
+
"li",
|
|
37
|
+
"blockquote",
|
|
38
|
+
"pre",
|
|
39
|
+
"code",
|
|
40
|
+
"a",
|
|
41
|
+
"img",
|
|
42
|
+
"table",
|
|
43
|
+
"thead",
|
|
44
|
+
"tbody",
|
|
45
|
+
"tfoot",
|
|
46
|
+
"tr",
|
|
47
|
+
"th",
|
|
48
|
+
"td",
|
|
49
|
+
"span",
|
|
50
|
+
"div"
|
|
51
|
+
];
|
|
52
|
+
const DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
53
|
+
a: ["href", "target", "rel", "title"],
|
|
54
|
+
img: [
|
|
55
|
+
"src",
|
|
56
|
+
"alt",
|
|
57
|
+
"title",
|
|
58
|
+
"width",
|
|
59
|
+
"height",
|
|
60
|
+
"class",
|
|
61
|
+
"loading",
|
|
62
|
+
"decoding"
|
|
63
|
+
],
|
|
64
|
+
blockquote: ["cite"],
|
|
65
|
+
code: ["class"],
|
|
66
|
+
pre: ["class"],
|
|
67
|
+
span: ["class", "style"],
|
|
68
|
+
div: ["class"],
|
|
69
|
+
table: ["class"],
|
|
70
|
+
th: ["colspan", "rowspan", "align"],
|
|
71
|
+
td: ["colspan", "rowspan", "align"]
|
|
72
|
+
};
|
|
73
|
+
function sanitizeHTML(html, options = {}) {
|
|
74
|
+
const {
|
|
75
|
+
allowedTags = DEFAULT_ALLOWED_TAGS,
|
|
76
|
+
allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES,
|
|
77
|
+
removeScripts = true,
|
|
78
|
+
removeStyles = true
|
|
79
|
+
} = options;
|
|
80
|
+
const config2 = {
|
|
81
|
+
ALLOWED_TAGS: allowedTags,
|
|
82
|
+
ALLOWED_ATTR: Object.keys(allowedAttributes).reduce((acc, tag) => {
|
|
83
|
+
allowedAttributes[tag].forEach((attr) => {
|
|
84
|
+
acc.push(attr);
|
|
85
|
+
});
|
|
86
|
+
return acc;
|
|
87
|
+
}, []),
|
|
88
|
+
KEEP_CONTENT: true,
|
|
89
|
+
ALLOW_DATA_ATTR: false,
|
|
90
|
+
FORBID_TAGS: removeScripts ? ["script", "style"] : [],
|
|
91
|
+
FORBID_ATTR: removeStyles ? ["style"] : []
|
|
92
|
+
};
|
|
93
|
+
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
|
94
|
+
var _a;
|
|
95
|
+
const tagName = (_a = node.tagName) == null ? void 0 : _a.toLowerCase();
|
|
96
|
+
if (tagName && allowedAttributes[tagName]) {
|
|
97
|
+
const allowedAttrs = allowedAttributes[tagName];
|
|
98
|
+
const attrs = node.attributes;
|
|
99
|
+
if (attrs) {
|
|
100
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
101
|
+
const attr = attrs[i];
|
|
102
|
+
if (!allowedAttrs.includes(attr.name)) {
|
|
103
|
+
node.removeAttribute(attr.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (tagName === "a") {
|
|
109
|
+
const href = node.getAttribute("href");
|
|
110
|
+
if (href && !isValidUrl(href)) {
|
|
111
|
+
node.removeAttribute("href");
|
|
112
|
+
}
|
|
113
|
+
const target = node.getAttribute("target");
|
|
114
|
+
if (target === "_blank") {
|
|
115
|
+
node.setAttribute("rel", "noopener noreferrer");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (tagName === "img") {
|
|
119
|
+
const src = node.getAttribute("src");
|
|
120
|
+
if (src && !isValidImageUrl(src)) {
|
|
121
|
+
node.removeAttribute("src");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const cleaned = DOMPurify.sanitize(html, config2);
|
|
126
|
+
DOMPurify.removeHook("afterSanitizeAttributes");
|
|
127
|
+
return String(cleaned);
|
|
128
|
+
}
|
|
129
|
+
function isValidUrl(url) {
|
|
130
|
+
try {
|
|
131
|
+
if (url.startsWith("/") || url.startsWith("#")) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const urlObj = new URL(url);
|
|
135
|
+
return ["http:", "https:", "mailto:"].includes(urlObj.protocol);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isValidImageUrl(url) {
|
|
141
|
+
if (url.startsWith("data:image/")) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (url.startsWith("/")) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return isValidUrl(url);
|
|
148
|
+
}
|
|
149
|
+
function useProductContent(options) {
|
|
150
|
+
const { productId, maxVersions = 10, autoSanitize = true } = options;
|
|
151
|
+
const PRODUCT_FIELDS = ["id", "title", "status", "handle", "metadata"].join(
|
|
152
|
+
","
|
|
153
|
+
);
|
|
154
|
+
const [product, setProduct] = useState(null);
|
|
155
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
156
|
+
const [error, setError] = useState(null);
|
|
157
|
+
const fetchProduct = async () => {
|
|
158
|
+
setIsLoading(true);
|
|
159
|
+
setError(null);
|
|
160
|
+
try {
|
|
161
|
+
const response = await sdk.admin.product.retrieve(productId, {
|
|
162
|
+
fields: PRODUCT_FIELDS
|
|
163
|
+
});
|
|
164
|
+
setProduct(response.product);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
try {
|
|
167
|
+
const fallback = await sdk.admin.product.retrieve(productId);
|
|
168
|
+
setProduct(fallback.product);
|
|
169
|
+
} catch (innerErr) {
|
|
170
|
+
const error2 = innerErr instanceof Error ? innerErr : err instanceof Error ? err : new Error("Failed to fetch product");
|
|
171
|
+
setError(error2);
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
setIsLoading(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
fetchProduct();
|
|
179
|
+
}, [productId]);
|
|
180
|
+
const updateProduct = async (data) => {
|
|
181
|
+
return await sdk.admin.product.update(productId, data);
|
|
182
|
+
};
|
|
183
|
+
const metadata = useMemo(() => {
|
|
184
|
+
if (!product || !product.metadata) return {};
|
|
185
|
+
return product.metadata;
|
|
186
|
+
}, [product]);
|
|
187
|
+
const longDescription = useMemo(() => {
|
|
188
|
+
return metadata.long_description || null;
|
|
189
|
+
}, [metadata.long_description]);
|
|
190
|
+
const specs = useMemo(() => {
|
|
191
|
+
return metadata.specs || null;
|
|
192
|
+
}, [metadata.specs]);
|
|
193
|
+
const versions = useMemo(() => {
|
|
194
|
+
return metadata.long_description_versions || [];
|
|
195
|
+
}, [metadata.long_description_versions]);
|
|
196
|
+
const updateLongDescription = useCallback(
|
|
197
|
+
async (html, richjson, locale = "th-TH") => {
|
|
198
|
+
const sanitizedHtml = autoSanitize ? sanitizeHTML(html) : html;
|
|
199
|
+
const newDescription = {
|
|
200
|
+
html: sanitizedHtml,
|
|
201
|
+
richjson,
|
|
202
|
+
locale,
|
|
203
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
204
|
+
updated_by: "current_user"
|
|
205
|
+
// TODO: Get from auth context
|
|
206
|
+
};
|
|
207
|
+
let newVersions = [...versions];
|
|
208
|
+
if (longDescription) {
|
|
209
|
+
const version = {
|
|
210
|
+
...longDescription,
|
|
211
|
+
version: versions.length + 1,
|
|
212
|
+
created_at: longDescription.updated_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
213
|
+
};
|
|
214
|
+
newVersions = [version, ...versions].slice(0, maxVersions);
|
|
215
|
+
}
|
|
216
|
+
await updateProduct({
|
|
217
|
+
metadata: {
|
|
218
|
+
...metadata,
|
|
219
|
+
long_description: newDescription,
|
|
220
|
+
long_description_versions: newVersions,
|
|
221
|
+
content_last_updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
content_updated_by: "current_user"
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
await fetchProduct();
|
|
226
|
+
},
|
|
227
|
+
[autoSanitize, longDescription, maxVersions, metadata, versions]
|
|
228
|
+
);
|
|
229
|
+
const updateSpecs = useCallback(
|
|
230
|
+
async (newSpecs) => {
|
|
231
|
+
await updateProduct({
|
|
232
|
+
metadata: {
|
|
233
|
+
...metadata,
|
|
234
|
+
specs: newSpecs,
|
|
235
|
+
content_last_updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
236
|
+
content_updated_by: "current_user"
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
await fetchProduct();
|
|
240
|
+
},
|
|
241
|
+
[metadata]
|
|
242
|
+
);
|
|
243
|
+
const createVersion = useCallback(
|
|
244
|
+
async (description) => {
|
|
245
|
+
const version = {
|
|
246
|
+
...description,
|
|
247
|
+
version: versions.length + 1,
|
|
248
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
249
|
+
};
|
|
250
|
+
const newVersions = [version, ...versions].slice(0, maxVersions);
|
|
251
|
+
await updateProduct({
|
|
252
|
+
metadata: {
|
|
253
|
+
...metadata,
|
|
254
|
+
long_description_versions: newVersions
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
await fetchProduct();
|
|
258
|
+
},
|
|
259
|
+
[maxVersions, metadata, versions]
|
|
260
|
+
);
|
|
261
|
+
const restoreVersion = useCallback(
|
|
262
|
+
async (versionNumber) => {
|
|
263
|
+
const version = versions.find((v) => v.version === versionNumber);
|
|
264
|
+
if (!version) {
|
|
265
|
+
throw new Error(`Version ${versionNumber} not found`);
|
|
266
|
+
}
|
|
267
|
+
const restoredDescription = {
|
|
268
|
+
html: version.html,
|
|
269
|
+
richjson: version.richjson,
|
|
270
|
+
locale: version.locale,
|
|
271
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
272
|
+
updated_by: "current_user"
|
|
273
|
+
};
|
|
274
|
+
await updateProduct({
|
|
275
|
+
metadata: {
|
|
276
|
+
...metadata,
|
|
277
|
+
long_description: restoredDescription,
|
|
278
|
+
content_last_updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
279
|
+
content_updated_by: "current_user"
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
await fetchProduct();
|
|
283
|
+
},
|
|
284
|
+
[metadata, versions]
|
|
285
|
+
);
|
|
286
|
+
const deleteVersion = useCallback(
|
|
287
|
+
async (versionNumber) => {
|
|
288
|
+
const newVersions = versions.filter((v) => v.version !== versionNumber);
|
|
289
|
+
await updateProduct({
|
|
290
|
+
metadata: {
|
|
291
|
+
...metadata,
|
|
292
|
+
long_description_versions: newVersions
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
await fetchProduct();
|
|
296
|
+
},
|
|
297
|
+
[metadata, versions]
|
|
298
|
+
);
|
|
299
|
+
const updateMetadata = useCallback(
|
|
300
|
+
async (updates) => {
|
|
301
|
+
await updateProduct({
|
|
302
|
+
metadata: {
|
|
303
|
+
...metadata,
|
|
304
|
+
...updates,
|
|
305
|
+
content_last_updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
306
|
+
content_updated_by: "current_user"
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
await fetchProduct();
|
|
310
|
+
},
|
|
311
|
+
[metadata]
|
|
312
|
+
);
|
|
313
|
+
return {
|
|
314
|
+
product,
|
|
315
|
+
isLoading,
|
|
316
|
+
error,
|
|
317
|
+
metadata,
|
|
318
|
+
longDescription,
|
|
319
|
+
specs,
|
|
320
|
+
versions,
|
|
321
|
+
updateLongDescription,
|
|
322
|
+
updateSpecs,
|
|
323
|
+
createVersion,
|
|
324
|
+
restoreVersion,
|
|
325
|
+
deleteVersion,
|
|
326
|
+
updateMetadata
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const DEFAULT_LOCALE$1 = "th-TH";
|
|
330
|
+
const ProductSpecsManager = ({
|
|
331
|
+
specs,
|
|
332
|
+
locale = DEFAULT_LOCALE$1,
|
|
333
|
+
onSave,
|
|
334
|
+
isSaving = false
|
|
335
|
+
}) => {
|
|
336
|
+
const [groups, setGroups] = useState(() => {
|
|
337
|
+
if (!specs || !specs.groups || specs.groups.length === 0) {
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
id: createRowId(),
|
|
341
|
+
key: "default",
|
|
342
|
+
label: "Specifications",
|
|
343
|
+
position: 1,
|
|
344
|
+
rows: [],
|
|
345
|
+
isExpanded: true,
|
|
346
|
+
isEditing: false
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
return specs.groups.sort((a, b) => (a.position || 0) - (b.position || 0)).map((group) => {
|
|
351
|
+
var _a, _b;
|
|
352
|
+
return {
|
|
353
|
+
id: createRowId(),
|
|
354
|
+
key: group.key,
|
|
355
|
+
label: ((_b = (_a = group.i18n) == null ? void 0 : _a[locale]) == null ? void 0 : _b.label) || group.key,
|
|
356
|
+
position: group.position || 1,
|
|
357
|
+
rows: group.items.map((item) => {
|
|
358
|
+
var _a2, _b2, _c, _d;
|
|
359
|
+
return {
|
|
360
|
+
id: createRowId(),
|
|
361
|
+
key: ((_b2 = (_a2 = item.i18n) == null ? void 0 : _a2[locale]) == null ? void 0 : _b2.key) || "",
|
|
362
|
+
value: ((_d = (_c = item.i18n) == null ? void 0 : _c[locale]) == null ? void 0 : _d.value) || "",
|
|
363
|
+
visible: item.visible !== false
|
|
364
|
+
};
|
|
365
|
+
}),
|
|
366
|
+
isExpanded: true,
|
|
367
|
+
isEditing: false
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
const [baselineGroups] = useState(groups);
|
|
372
|
+
const hasChanges = useMemo(() => {
|
|
373
|
+
return JSON.stringify(groups) !== JSON.stringify(baselineGroups);
|
|
374
|
+
}, [groups, baselineGroups]);
|
|
375
|
+
const addGroup = () => {
|
|
376
|
+
const newGroup = {
|
|
377
|
+
id: createRowId(),
|
|
378
|
+
key: `group-${Date.now()}`,
|
|
379
|
+
label: `New Group ${groups.length + 1}`,
|
|
380
|
+
position: groups.length + 1,
|
|
381
|
+
rows: [],
|
|
382
|
+
isExpanded: true,
|
|
383
|
+
isEditing: true
|
|
384
|
+
};
|
|
385
|
+
setGroups([...groups, newGroup]);
|
|
386
|
+
};
|
|
387
|
+
const updateGroup = (groupId, updates) => {
|
|
388
|
+
setGroups(
|
|
389
|
+
(prev) => prev.map((g) => g.id === groupId ? { ...g, ...updates } : g)
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
const deleteGroup = (groupId) => {
|
|
393
|
+
if (groups.length === 1) {
|
|
394
|
+
toast.error("Cannot delete the last group");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
setGroups((prev) => prev.filter((g) => g.id !== groupId));
|
|
398
|
+
};
|
|
399
|
+
const duplicateGroup = (groupId) => {
|
|
400
|
+
const group = groups.find((g) => g.id === groupId);
|
|
401
|
+
if (!group) return;
|
|
402
|
+
const newGroup = {
|
|
403
|
+
...group,
|
|
404
|
+
id: createRowId(),
|
|
405
|
+
key: `${group.key}-copy`,
|
|
406
|
+
label: `${group.label} (Copy)`,
|
|
407
|
+
position: groups.length + 1,
|
|
408
|
+
rows: group.rows.map((row) => ({ ...row, id: createRowId() }))
|
|
409
|
+
};
|
|
410
|
+
setGroups([...groups, newGroup]);
|
|
411
|
+
};
|
|
412
|
+
const moveGroup = (groupId, direction) => {
|
|
413
|
+
const index = groups.findIndex((g) => g.id === groupId);
|
|
414
|
+
if (index === -1) return;
|
|
415
|
+
if (direction === "up" && index === 0) return;
|
|
416
|
+
if (direction === "down" && index === groups.length - 1) return;
|
|
417
|
+
const newGroups = [...groups];
|
|
418
|
+
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
419
|
+
[newGroups[index], newGroups[targetIndex]] = [
|
|
420
|
+
newGroups[targetIndex],
|
|
421
|
+
newGroups[index]
|
|
422
|
+
];
|
|
423
|
+
newGroups.forEach((g, i) => {
|
|
424
|
+
g.position = i + 1;
|
|
425
|
+
});
|
|
426
|
+
setGroups(newGroups);
|
|
427
|
+
};
|
|
428
|
+
const addRow = (groupId) => {
|
|
429
|
+
updateGroup(groupId, {
|
|
430
|
+
rows: [
|
|
431
|
+
...groups.find((g) => g.id === groupId).rows,
|
|
432
|
+
{
|
|
433
|
+
id: createRowId(),
|
|
434
|
+
key: "",
|
|
435
|
+
value: "",
|
|
436
|
+
visible: true
|
|
437
|
+
}
|
|
438
|
+
]
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const updateRow = (groupId, rowId, updates) => {
|
|
442
|
+
const group = groups.find((g) => g.id === groupId);
|
|
443
|
+
if (!group) return;
|
|
444
|
+
updateGroup(groupId, {
|
|
445
|
+
rows: group.rows.map((r) => r.id === rowId ? { ...r, ...updates } : r)
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
const deleteRow = (groupId, rowId) => {
|
|
449
|
+
const group = groups.find((g) => g.id === groupId);
|
|
450
|
+
if (!group) return;
|
|
451
|
+
updateGroup(groupId, {
|
|
452
|
+
rows: group.rows.filter((r) => r.id !== rowId)
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
const duplicateRow = (groupId, rowId) => {
|
|
456
|
+
const group = groups.find((g) => g.id === groupId);
|
|
457
|
+
if (!group) return;
|
|
458
|
+
const row = group.rows.find((r) => r.id === rowId);
|
|
459
|
+
if (!row) return;
|
|
460
|
+
const newRow = {
|
|
461
|
+
...row,
|
|
462
|
+
id: createRowId()
|
|
463
|
+
};
|
|
464
|
+
updateGroup(groupId, {
|
|
465
|
+
rows: [...group.rows, newRow]
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
const moveRow = (groupId, rowId, direction) => {
|
|
469
|
+
const group = groups.find((g) => g.id === groupId);
|
|
470
|
+
if (!group) return;
|
|
471
|
+
const index = group.rows.findIndex((r) => r.id === rowId);
|
|
472
|
+
if (index === -1) return;
|
|
473
|
+
if (direction === "up" && index === 0) return;
|
|
474
|
+
if (direction === "down" && index === group.rows.length - 1) return;
|
|
475
|
+
const newRows = [...group.rows];
|
|
476
|
+
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
477
|
+
[newRows[index], newRows[targetIndex]] = [
|
|
478
|
+
newRows[targetIndex],
|
|
479
|
+
newRows[index]
|
|
480
|
+
];
|
|
481
|
+
updateGroup(groupId, { rows: newRows });
|
|
482
|
+
};
|
|
483
|
+
const clearAllRows = (groupId) => {
|
|
484
|
+
if (confirm("Are you sure you want to delete all attributes in this group?")) {
|
|
485
|
+
updateGroup(groupId, { rows: [] });
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const toggleAllVisible = (groupId, visible) => {
|
|
489
|
+
const group = groups.find((g) => g.id === groupId);
|
|
490
|
+
if (!group) return;
|
|
491
|
+
updateGroup(groupId, {
|
|
492
|
+
rows: group.rows.map((r) => ({ ...r, visible }))
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
const handleSave = async () => {
|
|
496
|
+
try {
|
|
497
|
+
for (const group of groups) {
|
|
498
|
+
if (!group.key.trim()) {
|
|
499
|
+
toast.error("All groups must have a key");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (!group.label.trim()) {
|
|
503
|
+
toast.error("All groups must have a label");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const payload = {
|
|
508
|
+
default_locale: locale,
|
|
509
|
+
groups: groups.map((group, groupIndex) => ({
|
|
510
|
+
key: group.key.trim(),
|
|
511
|
+
position: groupIndex + 1,
|
|
512
|
+
i18n: {
|
|
513
|
+
[locale]: {
|
|
514
|
+
label: group.label.trim()
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
items: group.rows.filter((row) => row.key.trim() || row.value.trim()).map((row, rowIndex) => ({
|
|
518
|
+
position: rowIndex + 1,
|
|
519
|
+
visible: row.visible,
|
|
520
|
+
i18n: {
|
|
521
|
+
[locale]: {
|
|
522
|
+
key: row.key.trim(),
|
|
523
|
+
value: row.value.trim()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}))
|
|
527
|
+
}))
|
|
528
|
+
};
|
|
529
|
+
await onSave(payload);
|
|
530
|
+
toast.success("Specifications saved successfully");
|
|
531
|
+
} catch (error) {
|
|
532
|
+
toast.error("Failed to save specifications");
|
|
533
|
+
console.error(error);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const handleReset = () => {
|
|
537
|
+
if (confirm("Are you sure you want to reset all changes?")) {
|
|
538
|
+
setGroups(baselineGroups);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
542
|
+
/* @__PURE__ */ jsx(Toaster, {}),
|
|
543
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
544
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
545
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-base", children: "Product Specifications" }),
|
|
546
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: "Manage custom attributes organized in groups" })
|
|
547
|
+
] }),
|
|
548
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "small", onClick: addGroup, children: [
|
|
549
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1 h-4 w-4" }),
|
|
550
|
+
"Add Group"
|
|
551
|
+
] })
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-4", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsxs(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
className: "rounded-lg border border-ui-border-base bg-ui-bg-base",
|
|
557
|
+
children: [
|
|
558
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-ui-border-base bg-ui-bg-subtle px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
559
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-1", children: [
|
|
560
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
561
|
+
/* @__PURE__ */ jsx(
|
|
562
|
+
IconButton,
|
|
563
|
+
{
|
|
564
|
+
size: "small",
|
|
565
|
+
variant: "transparent",
|
|
566
|
+
onClick: () => updateGroup(group.id, {
|
|
567
|
+
isExpanded: !group.isExpanded
|
|
568
|
+
}),
|
|
569
|
+
children: /* @__PURE__ */ jsx(ChevronUpDown, {})
|
|
570
|
+
}
|
|
571
|
+
),
|
|
572
|
+
/* @__PURE__ */ jsx(Badge, { size: "small", color: "blue", children: groupIndex + 1 })
|
|
573
|
+
] }),
|
|
574
|
+
group.isEditing ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-1", children: [
|
|
575
|
+
/* @__PURE__ */ jsx(
|
|
576
|
+
Input,
|
|
577
|
+
{
|
|
578
|
+
size: "small",
|
|
579
|
+
placeholder: "Group Key",
|
|
580
|
+
value: group.key,
|
|
581
|
+
onChange: (e) => updateGroup(group.id, { key: e.target.value }),
|
|
582
|
+
className: "w-40"
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
Input,
|
|
587
|
+
{
|
|
588
|
+
size: "small",
|
|
589
|
+
placeholder: "Group Label",
|
|
590
|
+
value: group.label,
|
|
591
|
+
onChange: (e) => updateGroup(group.id, { label: e.target.value }),
|
|
592
|
+
className: "flex-1"
|
|
593
|
+
}
|
|
594
|
+
),
|
|
595
|
+
/* @__PURE__ */ jsx(
|
|
596
|
+
IconButton,
|
|
597
|
+
{
|
|
598
|
+
size: "small",
|
|
599
|
+
variant: "transparent",
|
|
600
|
+
onClick: () => updateGroup(group.id, { isEditing: false }),
|
|
601
|
+
children: /* @__PURE__ */ jsx(XMark, {})
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
605
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm font-medium", children: group.label }),
|
|
606
|
+
/* @__PURE__ */ jsx(Badge, { size: "xsmall", color: "grey", children: group.key }),
|
|
607
|
+
/* @__PURE__ */ jsxs(Badge, { size: "xsmall", color: "green", children: [
|
|
608
|
+
group.rows.filter((r) => r.visible).length,
|
|
609
|
+
" visible"
|
|
610
|
+
] })
|
|
611
|
+
] })
|
|
612
|
+
] }),
|
|
613
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
614
|
+
/* @__PURE__ */ jsx(
|
|
615
|
+
IconButton,
|
|
616
|
+
{
|
|
617
|
+
size: "small",
|
|
618
|
+
variant: "transparent",
|
|
619
|
+
onClick: () => updateGroup(group.id, { isEditing: true }),
|
|
620
|
+
children: /* @__PURE__ */ jsx(PencilSquare, {})
|
|
621
|
+
}
|
|
622
|
+
),
|
|
623
|
+
/* @__PURE__ */ jsx(
|
|
624
|
+
IconButton,
|
|
625
|
+
{
|
|
626
|
+
size: "small",
|
|
627
|
+
variant: "transparent",
|
|
628
|
+
onClick: () => moveGroup(group.id, "up"),
|
|
629
|
+
disabled: groupIndex === 0,
|
|
630
|
+
children: "↑"
|
|
631
|
+
}
|
|
632
|
+
),
|
|
633
|
+
/* @__PURE__ */ jsx(
|
|
634
|
+
IconButton,
|
|
635
|
+
{
|
|
636
|
+
size: "small",
|
|
637
|
+
variant: "transparent",
|
|
638
|
+
onClick: () => moveGroup(group.id, "down"),
|
|
639
|
+
disabled: groupIndex === groups.length - 1,
|
|
640
|
+
children: "↓"
|
|
641
|
+
}
|
|
642
|
+
),
|
|
643
|
+
/* @__PURE__ */ jsx(
|
|
644
|
+
IconButton,
|
|
645
|
+
{
|
|
646
|
+
size: "small",
|
|
647
|
+
variant: "transparent",
|
|
648
|
+
onClick: () => duplicateGroup(group.id),
|
|
649
|
+
children: /* @__PURE__ */ jsx(EllipsisHorizontal, {})
|
|
650
|
+
}
|
|
651
|
+
),
|
|
652
|
+
/* @__PURE__ */ jsx(
|
|
653
|
+
IconButton,
|
|
654
|
+
{
|
|
655
|
+
size: "small",
|
|
656
|
+
variant: "transparent",
|
|
657
|
+
onClick: () => deleteGroup(group.id),
|
|
658
|
+
disabled: groups.length === 1,
|
|
659
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
660
|
+
}
|
|
661
|
+
)
|
|
662
|
+
] })
|
|
663
|
+
] }) }),
|
|
664
|
+
group.isExpanded && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
|
|
665
|
+
group.rows.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pb-2 border-b border-ui-border-base", children: [
|
|
666
|
+
/* @__PURE__ */ jsx(
|
|
667
|
+
Button,
|
|
668
|
+
{
|
|
669
|
+
variant: "secondary",
|
|
670
|
+
size: "small",
|
|
671
|
+
onClick: () => toggleAllVisible(group.id, true),
|
|
672
|
+
children: "Show All"
|
|
673
|
+
}
|
|
674
|
+
),
|
|
675
|
+
/* @__PURE__ */ jsx(
|
|
676
|
+
Button,
|
|
677
|
+
{
|
|
678
|
+
variant: "secondary",
|
|
679
|
+
size: "small",
|
|
680
|
+
onClick: () => toggleAllVisible(group.id, false),
|
|
681
|
+
children: "Hide All"
|
|
682
|
+
}
|
|
683
|
+
),
|
|
684
|
+
/* @__PURE__ */ jsx(
|
|
685
|
+
Button,
|
|
686
|
+
{
|
|
687
|
+
variant: "secondary",
|
|
688
|
+
size: "small",
|
|
689
|
+
onClick: () => clearAllRows(group.id),
|
|
690
|
+
children: "Clear All"
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
] }),
|
|
694
|
+
group.rows.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-lg border-2 border-dashed border-ui-border-base bg-ui-bg-subtle p-6 text-center", children: /* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-muted", children: "No attributes in this group yet" }) }),
|
|
695
|
+
group.rows.map((row, rowIndex) => /* @__PURE__ */ jsx(
|
|
696
|
+
"div",
|
|
697
|
+
{
|
|
698
|
+
className: "rounded-lg border border-ui-border-base bg-ui-bg-subtle p-3",
|
|
699
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
700
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
701
|
+
/* @__PURE__ */ jsxs(Badge, { size: "xsmall", color: "blue", children: [
|
|
702
|
+
"#",
|
|
703
|
+
rowIndex + 1
|
|
704
|
+
] }),
|
|
705
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
706
|
+
/* @__PURE__ */ jsx(
|
|
707
|
+
IconButton,
|
|
708
|
+
{
|
|
709
|
+
size: "small",
|
|
710
|
+
variant: "transparent",
|
|
711
|
+
onClick: () => moveRow(group.id, row.id, "up"),
|
|
712
|
+
disabled: rowIndex === 0,
|
|
713
|
+
children: "↑"
|
|
714
|
+
}
|
|
715
|
+
),
|
|
716
|
+
/* @__PURE__ */ jsx(
|
|
717
|
+
IconButton,
|
|
718
|
+
{
|
|
719
|
+
size: "small",
|
|
720
|
+
variant: "transparent",
|
|
721
|
+
onClick: () => moveRow(group.id, row.id, "down"),
|
|
722
|
+
disabled: rowIndex === group.rows.length - 1,
|
|
723
|
+
children: "↓"
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
] })
|
|
727
|
+
] }),
|
|
728
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 grid gap-3 md:grid-cols-2", children: [
|
|
729
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
730
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `key-${row.id}`, className: "text-xs", children: "Attribute Name" }),
|
|
731
|
+
/* @__PURE__ */ jsx(
|
|
732
|
+
Input,
|
|
733
|
+
{
|
|
734
|
+
id: `key-${row.id}`,
|
|
735
|
+
size: "small",
|
|
736
|
+
value: row.key,
|
|
737
|
+
onChange: (e) => updateRow(group.id, row.id, {
|
|
738
|
+
key: e.target.value
|
|
739
|
+
}),
|
|
740
|
+
placeholder: "e.g., Screen Size"
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
] }),
|
|
744
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
745
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `value-${row.id}`, className: "text-xs", children: "Value" }),
|
|
746
|
+
/* @__PURE__ */ jsx(
|
|
747
|
+
Input,
|
|
748
|
+
{
|
|
749
|
+
id: `value-${row.id}`,
|
|
750
|
+
size: "small",
|
|
751
|
+
value: row.value,
|
|
752
|
+
onChange: (e) => updateRow(group.id, row.id, {
|
|
753
|
+
value: e.target.value
|
|
754
|
+
}),
|
|
755
|
+
placeholder: "e.g., 15.6 inches"
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
] })
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 pt-6", children: [
|
|
761
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
|
|
762
|
+
/* @__PURE__ */ jsx(
|
|
763
|
+
Switch,
|
|
764
|
+
{
|
|
765
|
+
checked: row.visible,
|
|
766
|
+
onCheckedChange: (value) => updateRow(group.id, row.id, { visible: value })
|
|
767
|
+
}
|
|
768
|
+
),
|
|
769
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: row.visible ? "Visible" : "Hidden" })
|
|
770
|
+
] }),
|
|
771
|
+
/* @__PURE__ */ jsx(
|
|
772
|
+
IconButton,
|
|
773
|
+
{
|
|
774
|
+
size: "small",
|
|
775
|
+
variant: "transparent",
|
|
776
|
+
onClick: () => duplicateRow(group.id, row.id),
|
|
777
|
+
children: /* @__PURE__ */ jsx(EllipsisHorizontal, {})
|
|
778
|
+
}
|
|
779
|
+
),
|
|
780
|
+
/* @__PURE__ */ jsx(
|
|
781
|
+
IconButton,
|
|
782
|
+
{
|
|
783
|
+
size: "small",
|
|
784
|
+
variant: "transparent",
|
|
785
|
+
onClick: () => deleteRow(group.id, row.id),
|
|
786
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
] })
|
|
790
|
+
] })
|
|
791
|
+
},
|
|
792
|
+
row.id
|
|
793
|
+
)),
|
|
794
|
+
/* @__PURE__ */ jsxs(
|
|
795
|
+
Button,
|
|
796
|
+
{
|
|
797
|
+
variant: "secondary",
|
|
798
|
+
size: "small",
|
|
799
|
+
onClick: () => addRow(group.id),
|
|
800
|
+
className: "w-full",
|
|
801
|
+
children: [
|
|
802
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
|
|
803
|
+
"Add Attribute"
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
] })
|
|
808
|
+
]
|
|
809
|
+
},
|
|
810
|
+
group.id
|
|
811
|
+
)) }),
|
|
812
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 pt-4 border-t border-ui-border-base", children: [
|
|
813
|
+
/* @__PURE__ */ jsxs(
|
|
814
|
+
Button,
|
|
815
|
+
{
|
|
816
|
+
variant: "secondary",
|
|
817
|
+
onClick: handleReset,
|
|
818
|
+
disabled: !hasChanges || isSaving,
|
|
819
|
+
children: [
|
|
820
|
+
/* @__PURE__ */ jsx(ArrowUturnLeft, { className: "mr-2 h-4 w-4" }),
|
|
821
|
+
"Reset"
|
|
822
|
+
]
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsxs(Button, { onClick: handleSave, disabled: !hasChanges || isSaving, children: [
|
|
826
|
+
/* @__PURE__ */ jsx(ArrowDownTray, { className: "mr-2 h-4 w-4" }),
|
|
827
|
+
isSaving ? "Saving..." : "Save All Changes"
|
|
828
|
+
] })
|
|
829
|
+
] })
|
|
830
|
+
] });
|
|
831
|
+
};
|
|
832
|
+
function createRowId() {
|
|
833
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
834
|
+
return crypto.randomUUID();
|
|
835
|
+
}
|
|
836
|
+
return `row_${Math.random().toString(36).slice(2, 11)}`;
|
|
837
|
+
}
|
|
838
|
+
const DEFAULT_LOCALE = "th-TH";
|
|
839
|
+
const ProductContentWidget = ({ data }) => {
|
|
840
|
+
const {
|
|
841
|
+
product,
|
|
842
|
+
isLoading,
|
|
843
|
+
error,
|
|
844
|
+
longDescription,
|
|
845
|
+
specs,
|
|
846
|
+
metadata,
|
|
847
|
+
updateLongDescription,
|
|
848
|
+
updateSpecs
|
|
849
|
+
} = useProductContent({
|
|
850
|
+
productId: data.id
|
|
851
|
+
});
|
|
852
|
+
const [markdown, setMarkdown] = useState("");
|
|
853
|
+
const [baselineMarkdown, setBaselineMarkdown] = useState("");
|
|
854
|
+
const [descriptionSuccess, setDescriptionSuccess] = useState(
|
|
855
|
+
null
|
|
856
|
+
);
|
|
857
|
+
const [descriptionError, setDescriptionError] = useState(null);
|
|
858
|
+
const [savingDescription, setSavingDescription] = useState(false);
|
|
859
|
+
const markdownRef = useRef(null);
|
|
860
|
+
const [markdownTab, setMarkdownTab] = useState("write");
|
|
861
|
+
const [specLocale] = useState(DEFAULT_LOCALE);
|
|
862
|
+
const [savingSpecs, setSavingSpecs] = useState(false);
|
|
863
|
+
const hasDescriptionChanges = useMemo(() => {
|
|
864
|
+
const normalizedCurrent = markdown.trim();
|
|
865
|
+
const normalizedBaseline = baselineMarkdown.trim();
|
|
866
|
+
return normalizedCurrent !== normalizedBaseline;
|
|
867
|
+
}, [markdown, baselineMarkdown]);
|
|
868
|
+
useEffect(() => {
|
|
869
|
+
if (!longDescription) {
|
|
870
|
+
setMarkdown("");
|
|
871
|
+
setBaselineMarkdown("");
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const markdownContent = typeof longDescription.richjson === "object" && longDescription.richjson !== null && typeof longDescription.richjson.markdown === "string" ? longDescription.richjson.markdown : longDescription.html || "";
|
|
875
|
+
setMarkdown(markdownContent);
|
|
876
|
+
setBaselineMarkdown(markdownContent);
|
|
877
|
+
}, [longDescription]);
|
|
878
|
+
const statusBadges = useMemo(() => {
|
|
879
|
+
const badges = [];
|
|
880
|
+
if (longDescription == null ? void 0 : longDescription.html) {
|
|
881
|
+
badges.push(
|
|
882
|
+
/* @__PURE__ */ jsx(Badge, { color: "green", size: "small", children: "Description" }, "long-description")
|
|
883
|
+
);
|
|
884
|
+
} else {
|
|
885
|
+
badges.push(
|
|
886
|
+
/* @__PURE__ */ jsx(Badge, { color: "grey", size: "small", children: "Description" }, "long-description")
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (specs && specs.groups && specs.groups.length > 0) {
|
|
890
|
+
const hasItems = specs.groups.some((g) => g.items && g.items.length > 0);
|
|
891
|
+
badges.push(
|
|
892
|
+
/* @__PURE__ */ jsx(Badge, { color: hasItems ? "green" : "grey", size: "small", children: "Specs" }, "specs")
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
badges.push(
|
|
896
|
+
/* @__PURE__ */ jsx(Badge, { color: "grey", size: "small", children: "Specs" }, "specs")
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
return badges;
|
|
900
|
+
}, [longDescription == null ? void 0 : longDescription.html, specs]);
|
|
901
|
+
const handleInsertMarkdown = (before, after) => {
|
|
902
|
+
const textarea = markdownRef.current;
|
|
903
|
+
if (!textarea) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const start = textarea.selectionStart ?? 0;
|
|
907
|
+
const end = textarea.selectionEnd ?? 0;
|
|
908
|
+
const selection = markdown.substring(start, end);
|
|
909
|
+
const suffix = typeof after === "string" ? after : before;
|
|
910
|
+
const nextValue = markdown.substring(0, start) + before + selection + suffix + markdown.substring(end);
|
|
911
|
+
setMarkdown(nextValue);
|
|
912
|
+
requestAnimationFrame(() => {
|
|
913
|
+
textarea.focus();
|
|
914
|
+
const cursorStart = start + before.length;
|
|
915
|
+
const cursorEnd = cursorStart + selection.length;
|
|
916
|
+
textarea.setSelectionRange(
|
|
917
|
+
cursorStart,
|
|
918
|
+
typeof after === "string" ? cursorEnd : cursorStart
|
|
919
|
+
);
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
const handleMarkdownChange = (event) => {
|
|
923
|
+
setMarkdown(event.target.value);
|
|
924
|
+
setDescriptionError(null);
|
|
925
|
+
setDescriptionSuccess(null);
|
|
926
|
+
};
|
|
927
|
+
const handleSaveDescription = async () => {
|
|
928
|
+
if (!hasDescriptionChanges) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
setSavingDescription(true);
|
|
932
|
+
setDescriptionError(null);
|
|
933
|
+
setDescriptionSuccess(null);
|
|
934
|
+
try {
|
|
935
|
+
const html = convertMarkdownToHtml(markdown);
|
|
936
|
+
const locale = (longDescription == null ? void 0 : longDescription.locale) || specLocale || DEFAULT_LOCALE;
|
|
937
|
+
await updateLongDescription(html, { markdown }, locale);
|
|
938
|
+
setDescriptionSuccess("Long description saved");
|
|
939
|
+
setBaselineMarkdown(markdown);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
const message = err instanceof Error ? err.message : "Failed to save long description";
|
|
942
|
+
setDescriptionError(message);
|
|
943
|
+
} finally {
|
|
944
|
+
setSavingDescription(false);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const handleResetDescription = () => {
|
|
948
|
+
setMarkdown(baselineMarkdown);
|
|
949
|
+
setDescriptionError(null);
|
|
950
|
+
setDescriptionSuccess(null);
|
|
951
|
+
};
|
|
952
|
+
const descriptionPreview = useMemo(() => {
|
|
953
|
+
if (!markdown.trim()) {
|
|
954
|
+
return "";
|
|
955
|
+
}
|
|
956
|
+
return sanitizeHTML(convertMarkdownToHtml(markdown));
|
|
957
|
+
}, [markdown]);
|
|
958
|
+
if (error) {
|
|
959
|
+
return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Alert, { variant: "error", children: error.message }) });
|
|
960
|
+
}
|
|
961
|
+
if (isLoading && !product) {
|
|
962
|
+
return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading product content…" }) });
|
|
963
|
+
}
|
|
964
|
+
const lastUpdated = (metadata == null ? void 0 : metadata.content_last_updated) || (product == null ? void 0 : product.updated_at) || data.updated_at || null;
|
|
965
|
+
return /* @__PURE__ */ jsx(Container, { className: "divide-y px-0 pb-0", children: /* @__PURE__ */ jsxs("div", { className: "px-6 py-6 space-y-6", children: [
|
|
966
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 md:flex-row md:items-center md:justify-between", children: [
|
|
967
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
968
|
+
/* @__PURE__ */ jsx(DocumentText, { className: "h-6 w-6 text-ui-fg-subtle" }),
|
|
969
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
970
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Product Content" }),
|
|
971
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "Enrich the product page with a detailed description and structured specifications." })
|
|
972
|
+
] })
|
|
973
|
+
] }),
|
|
974
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: statusBadges })
|
|
975
|
+
] }),
|
|
976
|
+
lastUpdated && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-ui-bg-base px-3 py-2 text-xs text-ui-fg-subtle", children: [
|
|
977
|
+
"Last updated",
|
|
978
|
+
" ",
|
|
979
|
+
new Date(lastUpdated).toLocaleString(void 0, {
|
|
980
|
+
dateStyle: "medium",
|
|
981
|
+
timeStyle: "short"
|
|
982
|
+
}),
|
|
983
|
+
(metadata == null ? void 0 : metadata.content_updated_by) ? ` by ${metadata.content_updated_by}` : ""
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsxs(
|
|
986
|
+
Tabs,
|
|
987
|
+
{
|
|
988
|
+
value: markdownTab,
|
|
989
|
+
onValueChange: (val) => setMarkdownTab(val),
|
|
990
|
+
children: [
|
|
991
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
992
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "write", children: "Long Description" }),
|
|
993
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "preview", children: "Specifications" })
|
|
994
|
+
] }),
|
|
995
|
+
/* @__PURE__ */ jsxs(Tabs.Content, { value: "write", className: "mt-6 space-y-4", children: [
|
|
996
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-ui-bg-base p-4", children: [
|
|
997
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
998
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
999
|
+
/* @__PURE__ */ jsx(PencilSquare, { className: "h-5 w-5 text-ui-fg-subtle" }),
|
|
1000
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-base", children: "Rich Text (Markdown)" })
|
|
1001
|
+
] }),
|
|
1002
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: "Supports headings, lists, images, links and code blocks" })
|
|
1003
|
+
] }),
|
|
1004
|
+
descriptionError && /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, className: "mb-4", children: descriptionError }),
|
|
1005
|
+
descriptionSuccess && /* @__PURE__ */ jsx(Alert, { variant: "success", dismissible: true, className: "mb-4", children: descriptionSuccess }),
|
|
1006
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 border-b pb-4", children: [
|
|
1007
|
+
/* @__PURE__ */ jsx(
|
|
1008
|
+
Button,
|
|
1009
|
+
{
|
|
1010
|
+
type: "button",
|
|
1011
|
+
variant: "secondary",
|
|
1012
|
+
size: "small",
|
|
1013
|
+
onClick: () => handleInsertMarkdown("**", "**"),
|
|
1014
|
+
children: "Bold"
|
|
1015
|
+
}
|
|
1016
|
+
),
|
|
1017
|
+
/* @__PURE__ */ jsx(
|
|
1018
|
+
Button,
|
|
1019
|
+
{
|
|
1020
|
+
type: "button",
|
|
1021
|
+
variant: "secondary",
|
|
1022
|
+
size: "small",
|
|
1023
|
+
onClick: () => handleInsertMarkdown("*", "*"),
|
|
1024
|
+
children: "Italic"
|
|
1025
|
+
}
|
|
1026
|
+
),
|
|
1027
|
+
/* @__PURE__ */ jsx(
|
|
1028
|
+
Button,
|
|
1029
|
+
{
|
|
1030
|
+
type: "button",
|
|
1031
|
+
variant: "secondary",
|
|
1032
|
+
size: "small",
|
|
1033
|
+
onClick: () => handleInsertMarkdown("`", "`"),
|
|
1034
|
+
children: "Code"
|
|
1035
|
+
}
|
|
1036
|
+
),
|
|
1037
|
+
/* @__PURE__ */ jsx(
|
|
1038
|
+
Button,
|
|
1039
|
+
{
|
|
1040
|
+
type: "button",
|
|
1041
|
+
variant: "secondary",
|
|
1042
|
+
size: "small",
|
|
1043
|
+
onClick: () => handleInsertMarkdown("## "),
|
|
1044
|
+
children: "Heading"
|
|
1045
|
+
}
|
|
1046
|
+
),
|
|
1047
|
+
/* @__PURE__ */ jsx(
|
|
1048
|
+
Button,
|
|
1049
|
+
{
|
|
1050
|
+
type: "button",
|
|
1051
|
+
variant: "secondary",
|
|
1052
|
+
size: "small",
|
|
1053
|
+
onClick: () => handleInsertMarkdown("- "),
|
|
1054
|
+
children: "List"
|
|
1055
|
+
}
|
|
1056
|
+
),
|
|
1057
|
+
/* @__PURE__ */ jsx(
|
|
1058
|
+
Button,
|
|
1059
|
+
{
|
|
1060
|
+
type: "button",
|
|
1061
|
+
variant: "secondary",
|
|
1062
|
+
size: "small",
|
|
1063
|
+
onClick: () => handleInsertMarkdown("[", "](https://)"),
|
|
1064
|
+
children: "Link"
|
|
1065
|
+
}
|
|
1066
|
+
),
|
|
1067
|
+
/* @__PURE__ */ jsx(
|
|
1068
|
+
Button,
|
|
1069
|
+
{
|
|
1070
|
+
type: "button",
|
|
1071
|
+
variant: "secondary",
|
|
1072
|
+
size: "small",
|
|
1073
|
+
onClick: () => handleInsertMarkdown(""),
|
|
1074
|
+
children: "Image"
|
|
1075
|
+
}
|
|
1076
|
+
)
|
|
1077
|
+
] }),
|
|
1078
|
+
/* @__PURE__ */ jsx(
|
|
1079
|
+
Textarea,
|
|
1080
|
+
{
|
|
1081
|
+
ref: markdownRef,
|
|
1082
|
+
value: markdown,
|
|
1083
|
+
onChange: handleMarkdownChange,
|
|
1084
|
+
rows: 12,
|
|
1085
|
+
className: "mt-4",
|
|
1086
|
+
placeholder: "Tell the full story about this product..."
|
|
1087
|
+
}
|
|
1088
|
+
),
|
|
1089
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex flex-col gap-2 md:flex-row md:justify-end", children: [
|
|
1090
|
+
/* @__PURE__ */ jsxs(
|
|
1091
|
+
Button,
|
|
1092
|
+
{
|
|
1093
|
+
variant: "secondary",
|
|
1094
|
+
onClick: handleResetDescription,
|
|
1095
|
+
disabled: !hasDescriptionChanges || savingDescription,
|
|
1096
|
+
children: [
|
|
1097
|
+
/* @__PURE__ */ jsx(ArrowUturnLeft, { className: "mr-2 h-4 w-4" }),
|
|
1098
|
+
"Reset"
|
|
1099
|
+
]
|
|
1100
|
+
}
|
|
1101
|
+
),
|
|
1102
|
+
/* @__PURE__ */ jsxs(
|
|
1103
|
+
Button,
|
|
1104
|
+
{
|
|
1105
|
+
onClick: handleSaveDescription,
|
|
1106
|
+
disabled: !hasDescriptionChanges || savingDescription,
|
|
1107
|
+
children: [
|
|
1108
|
+
/* @__PURE__ */ jsx(ArrowDownTray, { className: "mr-2 h-4 w-4" }),
|
|
1109
|
+
savingDescription ? "Saving..." : "Save Description"
|
|
1110
|
+
]
|
|
1111
|
+
}
|
|
1112
|
+
)
|
|
1113
|
+
] })
|
|
1114
|
+
] }),
|
|
1115
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-ui-bg-subtle p-4", children: [
|
|
1116
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-base mb-2", children: "Live Preview" }),
|
|
1117
|
+
markdown.trim() ? /* @__PURE__ */ jsx(
|
|
1118
|
+
"div",
|
|
1119
|
+
{
|
|
1120
|
+
className: "prose prose-sm max-w-none",
|
|
1121
|
+
dangerouslySetInnerHTML: { __html: descriptionPreview }
|
|
1122
|
+
}
|
|
1123
|
+
) : /* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-muted", children: "Start writing to see a preview." })
|
|
1124
|
+
] })
|
|
1125
|
+
] }),
|
|
1126
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "preview", className: "mt-6", children: /* @__PURE__ */ jsx(
|
|
1127
|
+
ProductSpecsManager,
|
|
1128
|
+
{
|
|
1129
|
+
specs,
|
|
1130
|
+
locale: specLocale,
|
|
1131
|
+
onSave: updateSpecs,
|
|
1132
|
+
isSaving: savingSpecs
|
|
1133
|
+
}
|
|
1134
|
+
) })
|
|
1135
|
+
]
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
] }) });
|
|
1139
|
+
};
|
|
1140
|
+
defineWidgetConfig({
|
|
1141
|
+
zone: "product.details.after"
|
|
1142
|
+
});
|
|
1143
|
+
function convertMarkdownToHtml(markdown) {
|
|
1144
|
+
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1145
|
+
const lines = normalized.split("\n");
|
|
1146
|
+
const output = [];
|
|
1147
|
+
let listContext = null;
|
|
1148
|
+
let inCodeBlock = false;
|
|
1149
|
+
let codeLang = "";
|
|
1150
|
+
const codeBuffer = [];
|
|
1151
|
+
const closeList = () => {
|
|
1152
|
+
if (listContext === "unordered") {
|
|
1153
|
+
output.push("</ul>");
|
|
1154
|
+
}
|
|
1155
|
+
if (listContext === "ordered") {
|
|
1156
|
+
output.push("</ol>");
|
|
1157
|
+
}
|
|
1158
|
+
listContext = null;
|
|
1159
|
+
};
|
|
1160
|
+
const flushCodeBuffer = () => {
|
|
1161
|
+
if (!inCodeBlock || !codeBuffer.length) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const content = codeBuffer.join("\n");
|
|
1165
|
+
output.push(
|
|
1166
|
+
`<pre><code${codeLang ? ` class="language-${codeLang}"` : ""}>${content}</code></pre>`
|
|
1167
|
+
);
|
|
1168
|
+
codeBuffer.length = 0;
|
|
1169
|
+
inCodeBlock = false;
|
|
1170
|
+
codeLang = "";
|
|
1171
|
+
};
|
|
1172
|
+
const processInline = (value) => renderInline(value);
|
|
1173
|
+
for (const rawLine of lines) {
|
|
1174
|
+
const line = rawLine;
|
|
1175
|
+
const trimmed = line.trim();
|
|
1176
|
+
if (trimmed.startsWith("```")) {
|
|
1177
|
+
flushCodeBuffer();
|
|
1178
|
+
closeList();
|
|
1179
|
+
if (!inCodeBlock) {
|
|
1180
|
+
inCodeBlock = true;
|
|
1181
|
+
codeLang = trimmed.substring(3).trim();
|
|
1182
|
+
} else {
|
|
1183
|
+
flushCodeBuffer();
|
|
1184
|
+
}
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
if (inCodeBlock) {
|
|
1188
|
+
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1189
|
+
codeBuffer.push(escaped);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (!trimmed) {
|
|
1193
|
+
closeList();
|
|
1194
|
+
flushCodeBuffer();
|
|
1195
|
+
output.push("");
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
|
1199
|
+
if (headingMatch) {
|
|
1200
|
+
closeList();
|
|
1201
|
+
flushCodeBuffer();
|
|
1202
|
+
const level = headingMatch[1].length;
|
|
1203
|
+
const content = processInline(headingMatch[2].trim());
|
|
1204
|
+
output.push(`<h${level}>${content}</h${level}>`);
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const unorderedMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
1208
|
+
if (unorderedMatch) {
|
|
1209
|
+
flushCodeBuffer();
|
|
1210
|
+
if (listContext !== "unordered") {
|
|
1211
|
+
closeList();
|
|
1212
|
+
listContext = "unordered";
|
|
1213
|
+
output.push("<ul>");
|
|
1214
|
+
}
|
|
1215
|
+
output.push(`<li>${processInline(unorderedMatch[1].trim())}</li>`);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const orderedMatch = trimmed.match(/^\d+\.\s+(.*)$/);
|
|
1219
|
+
if (orderedMatch) {
|
|
1220
|
+
flushCodeBuffer();
|
|
1221
|
+
if (listContext !== "ordered") {
|
|
1222
|
+
closeList();
|
|
1223
|
+
listContext = "ordered";
|
|
1224
|
+
output.push("<ol>");
|
|
1225
|
+
}
|
|
1226
|
+
output.push(`<li>${processInline(orderedMatch[1].trim())}</li>`);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (trimmed.startsWith(">")) {
|
|
1230
|
+
closeList();
|
|
1231
|
+
flushCodeBuffer();
|
|
1232
|
+
const content = processInline(trimmed.replace(/^>\s?/, ""));
|
|
1233
|
+
output.push(`<blockquote>${content}</blockquote>`);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
closeList();
|
|
1237
|
+
flushCodeBuffer();
|
|
1238
|
+
output.push(`<p>${processInline(trimmed)}</p>`);
|
|
1239
|
+
}
|
|
1240
|
+
closeList();
|
|
1241
|
+
flushCodeBuffer();
|
|
1242
|
+
return output.filter((line, index, arr) => {
|
|
1243
|
+
if (line === "" && (index === 0 || arr[index - 1] === "")) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
return true;
|
|
1247
|
+
}).join("\n");
|
|
1248
|
+
}
|
|
1249
|
+
function renderInline(value) {
|
|
1250
|
+
const segments = [];
|
|
1251
|
+
let buffer = "";
|
|
1252
|
+
let index = 0;
|
|
1253
|
+
const flushBuffer = () => {
|
|
1254
|
+
if (!buffer) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
segments.push(applyTextFormatting(buffer));
|
|
1258
|
+
buffer = "";
|
|
1259
|
+
};
|
|
1260
|
+
while (index < value.length) {
|
|
1261
|
+
if (value[index] === "!" && value[index + 1] === "[") {
|
|
1262
|
+
const parsedImage = parseMarkdownLink(value, index, true);
|
|
1263
|
+
if (parsedImage) {
|
|
1264
|
+
flushBuffer();
|
|
1265
|
+
segments.push(parsedImage.html);
|
|
1266
|
+
index = parsedImage.nextIndex;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (value[index] === "[") {
|
|
1271
|
+
const parsedLink = parseMarkdownLink(value, index, false);
|
|
1272
|
+
if (parsedLink) {
|
|
1273
|
+
flushBuffer();
|
|
1274
|
+
segments.push(parsedLink.html);
|
|
1275
|
+
index = parsedLink.nextIndex;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
buffer += value[index];
|
|
1280
|
+
index += 1;
|
|
1281
|
+
}
|
|
1282
|
+
flushBuffer();
|
|
1283
|
+
return segments.join("");
|
|
1284
|
+
}
|
|
1285
|
+
function parseMarkdownLink(source, start, isImage) {
|
|
1286
|
+
const bracketStart = isImage ? start + 1 : start;
|
|
1287
|
+
if (source[bracketStart] !== "[") {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
const labelEnd = findClosingToken(source, bracketStart, "[", "]");
|
|
1291
|
+
if (labelEnd === -1) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
if (source[labelEnd + 1] !== "(") {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
const destinationEnd = findClosingToken(source, labelEnd + 1, "(", ")");
|
|
1298
|
+
if (destinationEnd === -1) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
const labelRaw = source.slice(bracketStart + 1, labelEnd);
|
|
1302
|
+
const destinationRaw = source.slice(labelEnd + 2, destinationEnd);
|
|
1303
|
+
const destination = splitDestination(destinationRaw);
|
|
1304
|
+
if (!destination || !destination.url) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
const { url, title } = destination;
|
|
1308
|
+
const escapedUrl = escapeHtml(url);
|
|
1309
|
+
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
1310
|
+
if (isImage) {
|
|
1311
|
+
return {
|
|
1312
|
+
html: `<img src="${escapedUrl}" alt="${escapeHtml(
|
|
1313
|
+
labelRaw
|
|
1314
|
+
)}"${titleAttr} class="max-w-full h-auto rounded-md" />`,
|
|
1315
|
+
nextIndex: destinationEnd + 1
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
html: `<a href="${escapedUrl}" class="text-ui-fg-interactive underline"${titleAttr}>${applyTextFormatting(
|
|
1320
|
+
labelRaw
|
|
1321
|
+
)}</a>`,
|
|
1322
|
+
nextIndex: destinationEnd + 1
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
function findClosingToken(source, start, open, close) {
|
|
1326
|
+
let depth = 0;
|
|
1327
|
+
for (let i = start; i < source.length; i++) {
|
|
1328
|
+
const char = source[i];
|
|
1329
|
+
if (char === "\\") {
|
|
1330
|
+
i += 1;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (char === open) {
|
|
1334
|
+
depth += 1;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
if (char === close) {
|
|
1338
|
+
depth -= 1;
|
|
1339
|
+
if (depth === 0) {
|
|
1340
|
+
return i;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return -1;
|
|
1345
|
+
}
|
|
1346
|
+
function splitDestination(raw) {
|
|
1347
|
+
let value = raw.trim();
|
|
1348
|
+
if (!value) {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
let url = "";
|
|
1352
|
+
let rest = "";
|
|
1353
|
+
if (value.startsWith("<")) {
|
|
1354
|
+
const angleEnd = value.indexOf(">");
|
|
1355
|
+
if (angleEnd === -1) {
|
|
1356
|
+
url = value.slice(1).trim();
|
|
1357
|
+
} else {
|
|
1358
|
+
url = value.slice(1, angleEnd).trim();
|
|
1359
|
+
rest = value.slice(angleEnd + 1).trim();
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
const splitIndex = findFirstWhitespaceOutsideQuotes(value);
|
|
1363
|
+
if (splitIndex === -1) {
|
|
1364
|
+
url = value;
|
|
1365
|
+
} else {
|
|
1366
|
+
url = value.slice(0, splitIndex).trim();
|
|
1367
|
+
rest = value.slice(splitIndex).trim();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (!url) {
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
const maybeTitle = extractTitle(rest);
|
|
1374
|
+
return maybeTitle !== void 0 ? { url, title: maybeTitle } : { url: rest ? `${url} ${rest}`.trim() : url };
|
|
1375
|
+
}
|
|
1376
|
+
function findFirstWhitespaceOutsideQuotes(value) {
|
|
1377
|
+
let quote = null;
|
|
1378
|
+
for (let i = 0; i < value.length; i++) {
|
|
1379
|
+
const char = value[i];
|
|
1380
|
+
if (quote) {
|
|
1381
|
+
if (char === "\\") {
|
|
1382
|
+
i += 1;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (char === quote || quote === ")" && char === ")") {
|
|
1386
|
+
quote = null;
|
|
1387
|
+
}
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (char === '"' || char === "'") {
|
|
1391
|
+
quote = char;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (char === "(") {
|
|
1395
|
+
quote = ")";
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (/\s/.test(char)) {
|
|
1399
|
+
return i;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return -1;
|
|
1403
|
+
}
|
|
1404
|
+
function extractTitle(input) {
|
|
1405
|
+
const trimmed = input.trim();
|
|
1406
|
+
if (!trimmed) {
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1409
|
+
if (trimmed.length < 2) {
|
|
1410
|
+
return void 0;
|
|
1411
|
+
}
|
|
1412
|
+
const first = trimmed[0];
|
|
1413
|
+
const last = trimmed[trimmed.length - 1];
|
|
1414
|
+
if (first === '"' && last === '"' || first === "'" && last === "'" || first === "(" && last === ")") {
|
|
1415
|
+
return trimmed.slice(1, -1).trim();
|
|
1416
|
+
}
|
|
1417
|
+
return void 0;
|
|
1418
|
+
}
|
|
1419
|
+
function applyTextFormatting(segment) {
|
|
1420
|
+
let formatted = segment;
|
|
1421
|
+
formatted = formatted.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
1422
|
+
formatted = formatted.replace(/__(.+?)__/g, "<strong>$1</strong>");
|
|
1423
|
+
formatted = formatted.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
|
1424
|
+
formatted = formatted.replace(/_(.+?)_/g, "<em>$1</em>");
|
|
1425
|
+
formatted = formatted.replace(/~~(.+?)~~/g, "<del>$1</del>");
|
|
1426
|
+
formatted = formatted.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
1427
|
+
return formatted;
|
|
1428
|
+
}
|
|
1429
|
+
function escapeHtml(value) {
|
|
1430
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1431
|
+
}
|
|
1432
|
+
const ProductContentPage = () => {
|
|
1433
|
+
const [products, setProducts] = useState([]);
|
|
1434
|
+
const [loading, setLoading] = useState(true);
|
|
1435
|
+
const [error, setError] = useState(null);
|
|
1436
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
1437
|
+
useEffect(() => {
|
|
1438
|
+
fetchProducts();
|
|
1439
|
+
}, []);
|
|
1440
|
+
const fetchProducts = async () => {
|
|
1441
|
+
setLoading(true);
|
|
1442
|
+
setError(null);
|
|
1443
|
+
try {
|
|
1444
|
+
const result = await sdk.admin.product.list({
|
|
1445
|
+
limit: 100,
|
|
1446
|
+
fields: "id,title,handle,status,metadata"
|
|
1447
|
+
});
|
|
1448
|
+
setProducts(result.products || []);
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
const message = err instanceof Error ? err.message : "Failed to fetch products";
|
|
1451
|
+
setError(message);
|
|
1452
|
+
console.error("Error fetching products:", err);
|
|
1453
|
+
} finally {
|
|
1454
|
+
setLoading(false);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
const handleManageContent = (productId) => {
|
|
1458
|
+
window.location.href = `/app/products/${productId}`;
|
|
1459
|
+
};
|
|
1460
|
+
const filteredProducts = products.filter((product) => {
|
|
1461
|
+
var _a, _b;
|
|
1462
|
+
if (!searchQuery) return true;
|
|
1463
|
+
const query = searchQuery.toLowerCase();
|
|
1464
|
+
return ((_a = product.title) == null ? void 0 : _a.toLowerCase().includes(query)) || ((_b = product.handle) == null ? void 0 : _b.toLowerCase().includes(query));
|
|
1465
|
+
});
|
|
1466
|
+
const getContentStatus = (product) => {
|
|
1467
|
+
var _a, _b;
|
|
1468
|
+
const hasLongDesc = !!((_a = product.metadata) == null ? void 0 : _a.long_description);
|
|
1469
|
+
const hasSpecs = !!((_b = product.metadata) == null ? void 0 : _b.specs);
|
|
1470
|
+
if (hasLongDesc && hasSpecs) return { label: "Complete", color: "green" };
|
|
1471
|
+
if (hasLongDesc || hasSpecs) return { label: "Partial", color: "orange" };
|
|
1472
|
+
return { label: "Empty", color: "grey" };
|
|
1473
|
+
};
|
|
1474
|
+
if (loading) {
|
|
1475
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("div", { className: "flex h-64 items-center justify-center", children: /* @__PURE__ */ jsx(Text, { children: "Loading products..." }) }) });
|
|
1476
|
+
}
|
|
1477
|
+
if (error) {
|
|
1478
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Alert, { variant: "error", dismissible: true, children: error }) });
|
|
1479
|
+
}
|
|
1480
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
1481
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-8", children: [
|
|
1482
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-4", children: /* @__PURE__ */ jsxs("div", { children: [
|
|
1483
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", className: "mb-2", children: "Product Content" }),
|
|
1484
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Manage long descriptions and specifications for all products" })
|
|
1485
|
+
] }) }),
|
|
1486
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxs("div", { className: "relative flex-1 max-w-md", children: [
|
|
1487
|
+
/* @__PURE__ */ jsx(
|
|
1488
|
+
Input,
|
|
1489
|
+
{
|
|
1490
|
+
placeholder: "Search products...",
|
|
1491
|
+
value: searchQuery,
|
|
1492
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
1493
|
+
className: "pl-10"
|
|
1494
|
+
}
|
|
1495
|
+
),
|
|
1496
|
+
/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })
|
|
1497
|
+
] }) })
|
|
1498
|
+
] }),
|
|
1499
|
+
filteredProducts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base", children: [
|
|
1500
|
+
/* @__PURE__ */ jsx(
|
|
1501
|
+
SquareTwoStack,
|
|
1502
|
+
{
|
|
1503
|
+
className: "mb-4 text-ui-fg-subtle",
|
|
1504
|
+
style: { width: "3rem", height: "3rem" }
|
|
1505
|
+
}
|
|
1506
|
+
),
|
|
1507
|
+
/* @__PURE__ */ jsx(Text, { className: "mb-2 text-lg font-medium", children: searchQuery ? "No products found" : "No products yet" }),
|
|
1508
|
+
/* @__PURE__ */ jsx(Text, { className: "mb-4 text-ui-fg-subtle", children: searchQuery ? "Try adjusting your search query" : "Create products to manage their content" })
|
|
1509
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
1510
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1511
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Product" }),
|
|
1512
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Handle" }),
|
|
1513
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
1514
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Content Status" }),
|
|
1515
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { className: "text-right", children: "Actions" })
|
|
1516
|
+
] }) }),
|
|
1517
|
+
/* @__PURE__ */ jsx(Table.Body, { children: filteredProducts.map((product) => {
|
|
1518
|
+
var _a, _b;
|
|
1519
|
+
const contentStatus = getContentStatus(product);
|
|
1520
|
+
return /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
1521
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Text, { className: "font-medium", children: product.title }) }),
|
|
1522
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Badge, { color: "blue", children: product.handle || "—" }) }),
|
|
1523
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(
|
|
1524
|
+
Badge,
|
|
1525
|
+
{
|
|
1526
|
+
color: product.status === "published" ? "green" : "grey",
|
|
1527
|
+
children: product.status || "draft"
|
|
1528
|
+
}
|
|
1529
|
+
) }),
|
|
1530
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1531
|
+
/* @__PURE__ */ jsx(Badge, { color: contentStatus.color, children: contentStatus.label }),
|
|
1532
|
+
((_a = product.metadata) == null ? void 0 : _a.long_description) && /* @__PURE__ */ jsx(Badge, { color: "green", size: "xsmall", children: "Description" }),
|
|
1533
|
+
((_b = product.metadata) == null ? void 0 : _b.specs) && /* @__PURE__ */ jsx(Badge, { color: "green", size: "xsmall", children: "Specs" })
|
|
1534
|
+
] }) }),
|
|
1535
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsxs(
|
|
1536
|
+
Button,
|
|
1537
|
+
{
|
|
1538
|
+
variant: "secondary",
|
|
1539
|
+
size: "small",
|
|
1540
|
+
onClick: () => handleManageContent(product.id),
|
|
1541
|
+
children: [
|
|
1542
|
+
/* @__PURE__ */ jsx(PencilSquare, { className: "mr-1" }),
|
|
1543
|
+
"View Product"
|
|
1544
|
+
]
|
|
1545
|
+
}
|
|
1546
|
+
) }) })
|
|
1547
|
+
] }, product.id);
|
|
1548
|
+
}) })
|
|
1549
|
+
] }) })
|
|
1550
|
+
] });
|
|
1551
|
+
};
|
|
1552
|
+
const config = defineRouteConfig({
|
|
1553
|
+
label: "Product Content",
|
|
1554
|
+
icon: SquareTwoStack
|
|
1555
|
+
});
|
|
1556
|
+
const widgetModule = { widgets: [
|
|
1557
|
+
{
|
|
1558
|
+
Component: ProductContentWidget,
|
|
1559
|
+
zone: ["product.details.after"]
|
|
1560
|
+
}
|
|
1561
|
+
] };
|
|
1562
|
+
const routeModule = {
|
|
1563
|
+
routes: [
|
|
1564
|
+
{
|
|
1565
|
+
Component: ProductContentPage,
|
|
1566
|
+
path: "/product-content"
|
|
1567
|
+
}
|
|
1568
|
+
]
|
|
1569
|
+
};
|
|
1570
|
+
const menuItemModule = {
|
|
1571
|
+
menuItems: [
|
|
1572
|
+
{
|
|
1573
|
+
label: config.label,
|
|
1574
|
+
icon: config.icon,
|
|
1575
|
+
path: "/product-content",
|
|
1576
|
+
nested: void 0
|
|
1577
|
+
}
|
|
1578
|
+
]
|
|
1579
|
+
};
|
|
1580
|
+
const formModule = { customFields: {} };
|
|
1581
|
+
const displayModule = {
|
|
1582
|
+
displays: {}
|
|
1583
|
+
};
|
|
1584
|
+
const i18nModule = { resources: {} };
|
|
1585
|
+
const plugin = {
|
|
1586
|
+
widgetModule,
|
|
1587
|
+
routeModule,
|
|
1588
|
+
menuItemModule,
|
|
1589
|
+
formModule,
|
|
1590
|
+
displayModule,
|
|
1591
|
+
i18nModule
|
|
1592
|
+
};
|
|
1593
|
+
export {
|
|
1594
|
+
plugin as default
|
|
1595
|
+
};
|