medusa-product-helper 0.0.28 → 0.0.30

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.
@@ -1,1801 +1,9 @@
1
1
  "use strict";
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
- const adminSdk = require("@medusajs/admin-sdk");
4
- const ui = require("@medusajs/ui");
5
3
  const react = require("react");
6
4
  const reactQuery = require("@tanstack/react-query");
7
- const METADATA_FIELD_TYPES = ["number", "text", "file", "bool"];
8
- const VALID_FIELD_TYPES = new Set(METADATA_FIELD_TYPES);
9
- const BOOLEAN_TRUES = /* @__PURE__ */ new Set(["true", "1", "yes", "y", "on"]);
10
- const BOOLEAN_FALSES = /* @__PURE__ */ new Set(["false", "0", "no", "n", "off"]);
11
- function normalizeMetadataDescriptors(input) {
12
- if (!Array.isArray(input)) return [];
13
- const seenKeys = /* @__PURE__ */ new Set();
14
- return input.filter(
15
- (item) => item && typeof item === "object"
16
- ).map((item) => {
17
- const key = normalizeKey(item.key);
18
- const type = normalizeType(item.type);
19
- const label = normalizeLabel(item.label);
20
- const filterable = !!item.filterable;
21
- return { key, type, label, filterable };
22
- }).filter(({ key, type }) => key && type && !seenKeys.has(key)).map(({ key, type, label, filterable }) => {
23
- seenKeys.add(key);
24
- return {
25
- key,
26
- type,
27
- ...label && { label },
28
- ...filterable && { filterable: true }
29
- };
30
- });
31
- }
32
- function buildInitialFormState(descriptors, metadata) {
33
- const base = metadata && typeof metadata === "object" ? metadata : {};
34
- return descriptors.reduce((acc, descriptor) => {
35
- acc[descriptor.key] = normalizeFormValue(descriptor, base[descriptor.key]);
36
- return acc;
37
- }, {});
38
- }
39
- function buildMetadataPayload({
40
- descriptors,
41
- values,
42
- originalMetadata
43
- }) {
44
- const base = originalMetadata && typeof originalMetadata === "object" ? { ...originalMetadata } : {};
45
- descriptors.forEach((descriptor) => {
46
- const coerced = coerceMetadataValue(descriptor, values[descriptor.key]);
47
- if (coerced === void 0) {
48
- delete base[descriptor.key];
49
- } else {
50
- base[descriptor.key] = coerced;
51
- }
52
- });
53
- return base;
54
- }
55
- function hasMetadataChanges({
56
- descriptors,
57
- values,
58
- originalMetadata
59
- }) {
60
- const next = buildMetadataPayload({ descriptors, values, originalMetadata });
61
- const prev = originalMetadata && typeof originalMetadata === "object" ? originalMetadata : {};
62
- return descriptors.some(({ key }) => !isDeepEqual(prev[key], next[key]));
63
- }
64
- function validateValueForDescriptor(descriptor, value) {
65
- if (descriptor.type === "number") {
66
- if (value == null || value === "") return void 0;
67
- const num = typeof value === "number" ? value : Number(String(value).trim());
68
- return isNaN(num) ? "Enter a valid number" : void 0;
69
- }
70
- if (descriptor.type === "file") {
71
- if (!value) return void 0;
72
- try {
73
- new URL(String(value).trim());
74
- return void 0;
75
- } catch {
76
- return "Enter a valid URL";
77
- }
78
- }
79
- return void 0;
80
- }
81
- function normalizeFormValue(descriptor, currentValue) {
82
- if (descriptor.type === "bool") return Boolean(currentValue);
83
- if ((descriptor.type === "number" || descriptor.type === "text") && typeof currentValue === "number") {
84
- return currentValue.toString();
85
- }
86
- if (typeof currentValue === "string" || typeof currentValue === "number") {
87
- return String(currentValue);
88
- }
89
- return "";
90
- }
91
- function coerceMetadataValue(descriptor, value) {
92
- if (value == null || value === "") return void 0;
93
- if (descriptor.type === "bool") {
94
- if (typeof value === "boolean") return value;
95
- if (typeof value === "number") return value !== 0;
96
- const normalized = String(value).trim().toLowerCase();
97
- if (!normalized) return void 0;
98
- if (BOOLEAN_TRUES.has(normalized)) return true;
99
- if (BOOLEAN_FALSES.has(normalized)) return false;
100
- return Boolean(value);
101
- }
102
- if (descriptor.type === "number") {
103
- const num = typeof value === "number" ? value : Number(String(value).trim());
104
- return isNaN(num) ? void 0 : num;
105
- }
106
- const trimmed = String(value).trim();
107
- if (!trimmed) return void 0;
108
- return trimmed;
109
- }
110
- function normalizeKey(value) {
111
- return typeof value === "string" ? value.trim() || void 0 : void 0;
112
- }
113
- function normalizeType(value) {
114
- if (typeof value !== "string") return void 0;
115
- const type = value.trim().toLowerCase();
116
- return VALID_FIELD_TYPES.has(type) ? type : void 0;
117
- }
118
- function normalizeLabel(value) {
119
- return typeof value === "string" ? value.trim() || void 0 : void 0;
120
- }
121
- function isDeepEqual(a, b) {
122
- if (a === b) return true;
123
- if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
124
- const aKeys = Object.keys(a);
125
- const bKeys = Object.keys(b);
126
- if (aKeys.length !== bKeys.length) return false;
127
- return aKeys.every(
128
- (key) => isDeepEqual(a[key], b[key])
129
- );
130
- }
131
- const CONFIG_ENDPOINT = "/admin/product-metadata-config";
132
- const QUERY_KEY = ["medusa-product-helper", "metadata-config"];
133
- const useMetadataConfig = (entity) => {
134
- return reactQuery.useQuery({
135
- queryKey: [...QUERY_KEY, entity],
136
- queryFn: async () => {
137
- const response = await fetch(`${CONFIG_ENDPOINT}?entity=${entity}`, {
138
- credentials: "include"
139
- });
140
- if (!response.ok) {
141
- throw new Error("Unable to load metadata configuration");
142
- }
143
- const payload = await response.json();
144
- return normalizeMetadataDescriptors(payload.metadataDescriptors);
145
- },
146
- staleTime: 5 * 60 * 1e3
147
- });
148
- };
149
- const useProductMetadataConfig = () => useMetadataConfig("product");
150
- const useCategoryMetadataConfig = () => useMetadataConfig("category");
151
- const useOrderMetadataConfig = () => useMetadataConfig("order");
152
- const useCollectionMetadataConfig = () => useMetadataConfig("collection");
153
- const CONFIG_DOCS_URL$3 = "https://docs.medusajs.com/admin/extension-points/widgets#product-category-details";
154
- const CategoryMetadataTableWidget = ({ data }) => {
155
- const { data: descriptors = [], isPending, isError } = useCategoryMetadataConfig();
156
- const categoryId = (data == null ? void 0 : data.id) ?? void 0;
157
- const [baselineMetadata, setBaselineMetadata] = react.useState(
158
- (data == null ? void 0 : data.metadata) ?? {}
159
- );
160
- const queryClient = reactQuery.useQueryClient();
161
- const previousCategoryIdRef = react.useRef(categoryId);
162
- const isInitializedRef = react.useRef(false);
163
- const dataRef = react.useRef(data);
164
- const descriptorsRef = react.useRef(descriptors);
165
- react.useEffect(() => {
166
- dataRef.current = data;
167
- descriptorsRef.current = descriptors;
168
- }, [data, descriptors]);
169
- react.useEffect(() => {
170
- var _a;
171
- if (previousCategoryIdRef.current === categoryId && isInitializedRef.current) {
172
- return;
173
- }
174
- const categoryIdChanged = previousCategoryIdRef.current !== categoryId;
175
- if (categoryIdChanged || !isInitializedRef.current) {
176
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? ((_a = dataRef.current) == null ? void 0 : _a.metadata) ?? {};
177
- const currentDescriptors = descriptorsRef.current.length > 0 ? descriptorsRef.current : descriptors;
178
- if (currentDescriptors.length === 0) {
179
- return;
180
- }
181
- previousCategoryIdRef.current = categoryId;
182
- setBaselineMetadata(currentMetadata);
183
- const newInitialState = buildInitialFormState(currentDescriptors, currentMetadata);
184
- setValues(newInitialState);
185
- isInitializedRef.current = true;
186
- }
187
- }, [categoryId]);
188
- const metadataStringRef = react.useRef("");
189
- react.useEffect(() => {
190
- const hasCategoryData = !!data;
191
- const descriptorsLoaded = descriptors.length > 0;
192
- const sameCategory = previousCategoryIdRef.current === categoryId;
193
- const notInitialized = !isInitializedRef.current;
194
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? {};
195
- const currentMetadataString = JSON.stringify(currentMetadata);
196
- const metadataChanged = currentMetadataString !== metadataStringRef.current;
197
- if (hasCategoryData && descriptorsLoaded && sameCategory && (notInitialized || metadataChanged)) {
198
- const newInitialState = buildInitialFormState(descriptors, currentMetadata);
199
- setBaselineMetadata(currentMetadata);
200
- setValues(newInitialState);
201
- metadataStringRef.current = currentMetadataString;
202
- isInitializedRef.current = true;
203
- }
204
- }, [data, descriptors.length, categoryId]);
205
- const initialState = react.useMemo(
206
- () => buildInitialFormState(descriptors, baselineMetadata),
207
- [descriptors, baselineMetadata]
208
- );
209
- const [values, setValues] = react.useState({});
210
- const [isSaving, setIsSaving] = react.useState(false);
211
- const errors = react.useMemo(() => {
212
- return descriptors.reduce((acc, descriptor) => {
213
- const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
214
- if (error) {
215
- acc[descriptor.key] = error;
216
- }
217
- return acc;
218
- }, {});
219
- }, [descriptors, values]);
220
- const hasErrors = Object.keys(errors).length > 0;
221
- const isDirty = react.useMemo(() => {
222
- return hasMetadataChanges({
223
- descriptors,
224
- values,
225
- originalMetadata: baselineMetadata
226
- });
227
- }, [descriptors, values, baselineMetadata]);
228
- const handleStringChange = (key, nextValue) => {
229
- setValues((prev) => ({
230
- ...prev,
231
- [key]: nextValue
232
- }));
233
- };
234
- const handleBooleanChange = (key, nextValue) => {
235
- setValues((prev) => ({
236
- ...prev,
237
- [key]: nextValue
238
- }));
239
- };
240
- const handleReset = () => {
241
- setValues(initialState);
242
- };
243
- const handleSubmit = async () => {
244
- if (!(data == null ? void 0 : data.id) || !descriptors.length) {
245
- return;
246
- }
247
- setIsSaving(true);
248
- try {
249
- const metadataPayload = buildMetadataPayload({
250
- descriptors,
251
- values,
252
- originalMetadata: baselineMetadata
253
- });
254
- const response = await fetch(`/admin/product-categories/${data.id}`, {
255
- method: "POST",
256
- credentials: "include",
257
- headers: {
258
- "Content-Type": "application/json"
259
- },
260
- body: JSON.stringify({
261
- metadata: metadataPayload
262
- })
263
- });
264
- if (!response.ok) {
265
- const payload = await response.json().catch(() => null);
266
- throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
267
- }
268
- const updated = await response.json();
269
- const nextMetadata = updated.product_category.metadata;
270
- setBaselineMetadata(nextMetadata);
271
- setValues(buildInitialFormState(descriptors, nextMetadata));
272
- ui.toast.success("Metadata saved");
273
- await queryClient.invalidateQueries({
274
- queryKey: ["product-categories"]
275
- });
276
- await queryClient.invalidateQueries({
277
- queryKey: ["product-category", data.id]
278
- });
279
- if (data.id) {
280
- queryClient.refetchQueries({
281
- queryKey: ["product-category", data.id]
282
- }).catch(() => {
283
- });
284
- }
285
- } catch (error) {
286
- ui.toast.error(error instanceof Error ? error.message : "Save failed");
287
- } finally {
288
- setIsSaving(false);
289
- }
290
- };
291
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
292
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
293
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
294
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
295
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
296
- ] }),
297
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
298
- ] }),
299
- isPending || !isInitializedRef.current || Object.keys(values).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
300
- "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
301
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
302
- "."
303
- ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
304
- "Provide a ",
305
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
306
- " array in the plugin options to control which keys show up here.",
307
- " ",
308
- /* @__PURE__ */ jsxRuntime.jsx(
309
- "a",
310
- {
311
- className: "text-ui-fg-interactive underline",
312
- href: CONFIG_DOCS_URL$3,
313
- target: "_blank",
314
- rel: "noreferrer",
315
- children: "Learn how to configure it."
316
- }
317
- )
318
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
319
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
320
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
321
- /* @__PURE__ */ jsxRuntime.jsx(
322
- "th",
323
- {
324
- scope: "col",
325
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
326
- children: "Label"
327
- }
328
- ),
329
- /* @__PURE__ */ jsxRuntime.jsx(
330
- "th",
331
- {
332
- scope: "col",
333
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
334
- children: "Value"
335
- }
336
- )
337
- ] }) }),
338
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
339
- const value = values[descriptor.key];
340
- const error = errors[descriptor.key];
341
- return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
342
- /* @__PURE__ */ jsxRuntime.jsx(
343
- "th",
344
- {
345
- scope: "row",
346
- className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
347
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
348
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
349
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
350
- ] })
351
- }
352
- ),
353
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
354
- /* @__PURE__ */ jsxRuntime.jsx(
355
- ValueField$3,
356
- {
357
- descriptor,
358
- value,
359
- onStringChange: handleStringChange,
360
- onBooleanChange: handleBooleanChange
361
- }
362
- ),
363
- error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
364
- ] }) })
365
- ] }, descriptor.key);
366
- }) })
367
- ] }) }),
368
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 border-t border-ui-border-subtle pt-3 md:flex-row md:items-center md:justify-between", children: [
369
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Changes are stored on the category metadata object. Clearing a field removes the corresponding key on save." }),
370
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
371
- /* @__PURE__ */ jsxRuntime.jsx(
372
- ui.Button,
373
- {
374
- variant: "secondary",
375
- size: "small",
376
- disabled: !isDirty || isSaving,
377
- onClick: handleReset,
378
- children: "Reset"
379
- }
380
- ),
381
- /* @__PURE__ */ jsxRuntime.jsx(
382
- ui.Button,
383
- {
384
- size: "small",
385
- onClick: handleSubmit,
386
- disabled: !isDirty || hasErrors || isSaving,
387
- isLoading: isSaving,
388
- children: "Save metadata"
389
- }
390
- )
391
- ] })
392
- ] })
393
- ] })
394
- ] });
395
- };
396
- const ValueField$3 = ({
397
- descriptor,
398
- value,
399
- onStringChange,
400
- onBooleanChange
401
- }) => {
402
- const fileInputRef = react.useRef(null);
403
- const [isUploading, setIsUploading] = react.useState(false);
404
- const handleFileUpload = async (event) => {
405
- var _a;
406
- const file = (_a = event.target.files) == null ? void 0 : _a[0];
407
- if (!file) {
408
- return;
409
- }
410
- setIsUploading(true);
411
- try {
412
- const formData = new FormData();
413
- formData.append("files", file);
414
- const response = await fetch("/admin/uploads", {
415
- method: "POST",
416
- credentials: "include",
417
- body: formData
418
- });
419
- if (!response.ok) {
420
- const payload = await response.json().catch(() => null);
421
- throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
422
- }
423
- const result = await response.json();
424
- if (result.files && result.files.length > 0) {
425
- const uploadedFile = result.files[0];
426
- const fileUrl = uploadedFile.url || uploadedFile.key;
427
- if (fileUrl) {
428
- onStringChange(descriptor.key, fileUrl);
429
- ui.toast.success("File uploaded successfully");
430
- } else {
431
- throw new Error("File upload succeeded but no URL returned");
432
- }
433
- } else {
434
- throw new Error("File upload failed - no files returned");
435
- }
436
- } catch (error) {
437
- ui.toast.error(
438
- error instanceof Error ? error.message : "Failed to upload file"
439
- );
440
- } finally {
441
- setIsUploading(false);
442
- if (fileInputRef.current) {
443
- fileInputRef.current.value = "";
444
- }
445
- }
446
- };
447
- if (descriptor.type === "bool") {
448
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
449
- /* @__PURE__ */ jsxRuntime.jsx(
450
- ui.Switch,
451
- {
452
- checked: Boolean(value),
453
- onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
454
- "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
455
- }
456
- ),
457
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
458
- ] });
459
- }
460
- if (descriptor.type === "text") {
461
- return /* @__PURE__ */ jsxRuntime.jsx(
462
- ui.Textarea,
463
- {
464
- value: value ?? "",
465
- placeholder: "Enter text",
466
- rows: 3,
467
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
468
- }
469
- );
470
- }
471
- if (descriptor.type === "number") {
472
- return /* @__PURE__ */ jsxRuntime.jsx(
473
- ui.Input,
474
- {
475
- type: "text",
476
- inputMode: "decimal",
477
- placeholder: "0.00",
478
- value: value ?? "",
479
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
480
- }
481
- );
482
- }
483
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
484
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
485
- /* @__PURE__ */ jsxRuntime.jsx(
486
- ui.Input,
487
- {
488
- type: "url",
489
- placeholder: "https://example.com/file",
490
- value: value ?? "",
491
- onChange: (event) => onStringChange(descriptor.key, event.target.value),
492
- className: "flex-1"
493
- }
494
- ),
495
- /* @__PURE__ */ jsxRuntime.jsx(
496
- "input",
497
- {
498
- ref: fileInputRef,
499
- type: "file",
500
- className: "hidden",
501
- onChange: handleFileUpload,
502
- disabled: isUploading,
503
- "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
504
- }
505
- ),
506
- /* @__PURE__ */ jsxRuntime.jsx(
507
- ui.Button,
508
- {
509
- type: "button",
510
- variant: "secondary",
511
- size: "small",
512
- onClick: () => {
513
- var _a;
514
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
515
- },
516
- disabled: isUploading,
517
- isLoading: isUploading,
518
- children: isUploading ? "Uploading..." : "Upload"
519
- }
520
- )
521
- ] }),
522
- typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
523
- "a",
524
- {
525
- className: "txt-compact-small-plus text-ui-fg-interactive underline",
526
- href: value,
527
- target: "_blank",
528
- rel: "noreferrer",
529
- children: "View file"
530
- }
531
- )
532
- ] });
533
- };
534
- adminSdk.defineWidgetConfig({
535
- zone: "product_category.details.after"
536
- });
537
- const CONFIG_DOCS_URL$2 = "https://docs.medusajs.com/admin/extension-points/widgets#product-collection-details";
538
- const CollectionMetadataTableWidget = ({ data }) => {
539
- const { data: descriptors = [], isPending, isError } = useCollectionMetadataConfig();
540
- const collectionId = (data == null ? void 0 : data.id) ?? void 0;
541
- const [baselineMetadata, setBaselineMetadata] = react.useState(
542
- (data == null ? void 0 : data.metadata) ?? {}
543
- );
544
- const queryClient = reactQuery.useQueryClient();
545
- const previousCollectionIdRef = react.useRef(collectionId);
546
- const isInitializedRef = react.useRef(false);
547
- const dataRef = react.useRef(data);
548
- const descriptorsRef = react.useRef(descriptors);
549
- react.useEffect(() => {
550
- dataRef.current = data;
551
- descriptorsRef.current = descriptors;
552
- }, [data, descriptors]);
553
- react.useEffect(() => {
554
- var _a;
555
- if (previousCollectionIdRef.current === collectionId && isInitializedRef.current) {
556
- return;
557
- }
558
- const collectionIdChanged = previousCollectionIdRef.current !== collectionId;
559
- if (collectionIdChanged || !isInitializedRef.current) {
560
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? ((_a = dataRef.current) == null ? void 0 : _a.metadata) ?? {};
561
- const currentDescriptors = descriptorsRef.current.length > 0 ? descriptorsRef.current : descriptors;
562
- if (currentDescriptors.length === 0) {
563
- return;
564
- }
565
- previousCollectionIdRef.current = collectionId;
566
- setBaselineMetadata(currentMetadata);
567
- const newInitialState = buildInitialFormState(currentDescriptors, currentMetadata);
568
- setValues(newInitialState);
569
- isInitializedRef.current = true;
570
- }
571
- }, [collectionId]);
572
- const metadataStringRef = react.useRef("");
573
- react.useEffect(() => {
574
- const hasCollectionData = !!data;
575
- const descriptorsLoaded = descriptors.length > 0;
576
- const sameCollection = previousCollectionIdRef.current === collectionId;
577
- const notInitialized = !isInitializedRef.current;
578
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? {};
579
- const currentMetadataString = JSON.stringify(currentMetadata);
580
- const metadataChanged = currentMetadataString !== metadataStringRef.current;
581
- if (hasCollectionData && descriptorsLoaded && sameCollection && (notInitialized || metadataChanged)) {
582
- const newInitialState = buildInitialFormState(descriptors, currentMetadata);
583
- setBaselineMetadata(currentMetadata);
584
- setValues(newInitialState);
585
- metadataStringRef.current = currentMetadataString;
586
- isInitializedRef.current = true;
587
- }
588
- }, [data, descriptors.length, collectionId]);
589
- const initialState = react.useMemo(
590
- () => buildInitialFormState(descriptors, baselineMetadata),
591
- [descriptors, baselineMetadata]
592
- );
593
- const [values, setValues] = react.useState({});
594
- const [isSaving, setIsSaving] = react.useState(false);
595
- const errors = react.useMemo(() => {
596
- return descriptors.reduce((acc, descriptor) => {
597
- const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
598
- if (error) {
599
- acc[descriptor.key] = error;
600
- }
601
- return acc;
602
- }, {});
603
- }, [descriptors, values]);
604
- const hasErrors = Object.keys(errors).length > 0;
605
- const isDirty = react.useMemo(() => {
606
- return hasMetadataChanges({
607
- descriptors,
608
- values,
609
- originalMetadata: baselineMetadata
610
- });
611
- }, [descriptors, values, baselineMetadata]);
612
- const handleStringChange = (key, nextValue) => {
613
- setValues((prev) => ({
614
- ...prev,
615
- [key]: nextValue
616
- }));
617
- };
618
- const handleBooleanChange = (key, nextValue) => {
619
- setValues((prev) => ({
620
- ...prev,
621
- [key]: nextValue
622
- }));
623
- };
624
- const handleReset = () => {
625
- setValues(initialState);
626
- };
627
- const handleSubmit = async () => {
628
- if (!(data == null ? void 0 : data.id) || !descriptors.length) {
629
- return;
630
- }
631
- setIsSaving(true);
632
- try {
633
- const metadataPayload = buildMetadataPayload({
634
- descriptors,
635
- values,
636
- originalMetadata: baselineMetadata
637
- });
638
- const response = await fetch(`/admin/collections/${data.id}`, {
639
- method: "POST",
640
- credentials: "include",
641
- headers: {
642
- "Content-Type": "application/json"
643
- },
644
- body: JSON.stringify({
645
- metadata: metadataPayload
646
- })
647
- });
648
- if (!response.ok) {
649
- const payload = await response.json().catch(() => null);
650
- throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
651
- }
652
- const updated = await response.json();
653
- const nextMetadata = updated.product_collection.metadata;
654
- setBaselineMetadata(nextMetadata);
655
- setValues(buildInitialFormState(descriptors, nextMetadata));
656
- ui.toast.success("Metadata saved");
657
- await queryClient.invalidateQueries({
658
- queryKey: ["collections"]
659
- });
660
- await queryClient.invalidateQueries({
661
- queryKey: ["collection", data.id]
662
- });
663
- if (data.id) {
664
- queryClient.refetchQueries({
665
- queryKey: ["collection", data.id]
666
- }).catch(() => {
667
- });
668
- }
669
- } catch (error) {
670
- ui.toast.error(error instanceof Error ? error.message : "Save failed");
671
- } finally {
672
- setIsSaving(false);
673
- }
674
- };
675
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
676
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
677
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
678
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
679
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
680
- ] }),
681
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
682
- ] }),
683
- isPending || !isInitializedRef.current || Object.keys(values).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
684
- "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
685
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
686
- "."
687
- ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
688
- "Provide a ",
689
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
690
- " array in the plugin options to control which keys show up here.",
691
- " ",
692
- /* @__PURE__ */ jsxRuntime.jsx(
693
- "a",
694
- {
695
- className: "text-ui-fg-interactive underline",
696
- href: CONFIG_DOCS_URL$2,
697
- target: "_blank",
698
- rel: "noreferrer",
699
- children: "Learn how to configure it."
700
- }
701
- )
702
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
703
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
704
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
705
- /* @__PURE__ */ jsxRuntime.jsx(
706
- "th",
707
- {
708
- scope: "col",
709
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
710
- children: "Label"
711
- }
712
- ),
713
- /* @__PURE__ */ jsxRuntime.jsx(
714
- "th",
715
- {
716
- scope: "col",
717
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
718
- children: "Value"
719
- }
720
- )
721
- ] }) }),
722
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
723
- const value = values[descriptor.key];
724
- const error = errors[descriptor.key];
725
- return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
726
- /* @__PURE__ */ jsxRuntime.jsx(
727
- "th",
728
- {
729
- scope: "row",
730
- className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
731
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
732
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
733
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
734
- ] })
735
- }
736
- ),
737
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
738
- /* @__PURE__ */ jsxRuntime.jsx(
739
- ValueField$2,
740
- {
741
- descriptor,
742
- value,
743
- onStringChange: handleStringChange,
744
- onBooleanChange: handleBooleanChange
745
- }
746
- ),
747
- error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
748
- ] }) })
749
- ] }, descriptor.key);
750
- }) })
751
- ] }) }),
752
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 border-t border-ui-border-subtle pt-3 md:flex-row md:items-center md:justify-between", children: [
753
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Changes are stored on the collection metadata object. Clearing a field removes the corresponding key on save." }),
754
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
755
- /* @__PURE__ */ jsxRuntime.jsx(
756
- ui.Button,
757
- {
758
- variant: "secondary",
759
- size: "small",
760
- disabled: !isDirty || isSaving,
761
- onClick: handleReset,
762
- children: "Reset"
763
- }
764
- ),
765
- /* @__PURE__ */ jsxRuntime.jsx(
766
- ui.Button,
767
- {
768
- size: "small",
769
- onClick: handleSubmit,
770
- disabled: !isDirty || hasErrors || isSaving,
771
- isLoading: isSaving,
772
- children: "Save metadata"
773
- }
774
- )
775
- ] })
776
- ] })
777
- ] })
778
- ] });
779
- };
780
- const ValueField$2 = ({
781
- descriptor,
782
- value,
783
- onStringChange,
784
- onBooleanChange
785
- }) => {
786
- const fileInputRef = react.useRef(null);
787
- const [isUploading, setIsUploading] = react.useState(false);
788
- const handleFileUpload = async (event) => {
789
- var _a;
790
- const file = (_a = event.target.files) == null ? void 0 : _a[0];
791
- if (!file) {
792
- return;
793
- }
794
- setIsUploading(true);
795
- try {
796
- const formData = new FormData();
797
- formData.append("files", file);
798
- const response = await fetch("/admin/uploads", {
799
- method: "POST",
800
- credentials: "include",
801
- body: formData
802
- });
803
- if (!response.ok) {
804
- const payload = await response.json().catch(() => null);
805
- throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
806
- }
807
- const result = await response.json();
808
- if (result.files && result.files.length > 0) {
809
- const uploadedFile = result.files[0];
810
- const fileUrl = uploadedFile.url || uploadedFile.key;
811
- if (fileUrl) {
812
- onStringChange(descriptor.key, fileUrl);
813
- ui.toast.success("File uploaded successfully");
814
- } else {
815
- throw new Error("File upload succeeded but no URL returned");
816
- }
817
- } else {
818
- throw new Error("File upload failed - no files returned");
819
- }
820
- } catch (error) {
821
- ui.toast.error(
822
- error instanceof Error ? error.message : "Failed to upload file"
823
- );
824
- } finally {
825
- setIsUploading(false);
826
- if (fileInputRef.current) {
827
- fileInputRef.current.value = "";
828
- }
829
- }
830
- };
831
- if (descriptor.type === "bool") {
832
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
833
- /* @__PURE__ */ jsxRuntime.jsx(
834
- ui.Switch,
835
- {
836
- checked: Boolean(value),
837
- onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
838
- "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
839
- }
840
- ),
841
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
842
- ] });
843
- }
844
- if (descriptor.type === "text") {
845
- return /* @__PURE__ */ jsxRuntime.jsx(
846
- ui.Textarea,
847
- {
848
- value: value ?? "",
849
- placeholder: "Enter text",
850
- rows: 3,
851
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
852
- }
853
- );
854
- }
855
- if (descriptor.type === "number") {
856
- return /* @__PURE__ */ jsxRuntime.jsx(
857
- ui.Input,
858
- {
859
- type: "text",
860
- inputMode: "decimal",
861
- placeholder: "0.00",
862
- value: value ?? "",
863
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
864
- }
865
- );
866
- }
867
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
868
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
869
- /* @__PURE__ */ jsxRuntime.jsx(
870
- ui.Input,
871
- {
872
- type: "url",
873
- placeholder: "https://example.com/file",
874
- value: value ?? "",
875
- onChange: (event) => onStringChange(descriptor.key, event.target.value),
876
- className: "flex-1"
877
- }
878
- ),
879
- /* @__PURE__ */ jsxRuntime.jsx(
880
- "input",
881
- {
882
- ref: fileInputRef,
883
- type: "file",
884
- className: "hidden",
885
- onChange: handleFileUpload,
886
- disabled: isUploading,
887
- "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
888
- }
889
- ),
890
- /* @__PURE__ */ jsxRuntime.jsx(
891
- ui.Button,
892
- {
893
- type: "button",
894
- variant: "secondary",
895
- size: "small",
896
- onClick: () => {
897
- var _a;
898
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
899
- },
900
- disabled: isUploading,
901
- isLoading: isUploading,
902
- children: isUploading ? "Uploading..." : "Upload"
903
- }
904
- )
905
- ] }),
906
- typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
907
- "a",
908
- {
909
- className: "txt-compact-small-plus text-ui-fg-interactive underline",
910
- href: value,
911
- target: "_blank",
912
- rel: "noreferrer",
913
- children: "View file"
914
- }
915
- )
916
- ] });
917
- };
918
- adminSdk.defineWidgetConfig({
919
- zone: "product_collection.details.after"
920
- });
921
- const HideCategoryDefaultMetadataWidget = () => {
922
- react.useEffect(() => {
923
- const hideMetadataSection = () => {
924
- const headings = document.querySelectorAll("h2");
925
- headings.forEach((heading) => {
926
- var _a;
927
- if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
928
- let container = heading.parentElement;
929
- while (container && container !== document.body) {
930
- const hasContainerClass = container.classList.toString().includes("Container");
931
- const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
932
- if (hasContainerClass || isInSidebar) {
933
- const editLink = container.querySelector('a[href*="metadata/edit"]');
934
- const badge = container.querySelector('div[class*="Badge"]');
935
- if (editLink && badge) {
936
- container.style.display = "none";
937
- container.setAttribute("data-category-metadata-hidden", "true");
938
- return;
939
- }
940
- }
941
- container = container.parentElement;
942
- }
943
- }
944
- });
945
- };
946
- const runHide = () => {
947
- setTimeout(hideMetadataSection, 100);
948
- };
949
- runHide();
950
- const observer = new MutationObserver(() => {
951
- const alreadyHidden = document.querySelector(
952
- '[data-category-metadata-hidden="true"]'
953
- );
954
- if (!alreadyHidden) {
955
- runHide();
956
- }
957
- });
958
- observer.observe(document.body, {
959
- childList: true,
960
- subtree: true
961
- });
962
- return () => {
963
- observer.disconnect();
964
- const hidden = document.querySelector(
965
- '[data-category-metadata-hidden="true"]'
966
- );
967
- if (hidden) {
968
- hidden.style.display = "";
969
- hidden.removeAttribute("data-category-metadata-hidden");
970
- }
971
- };
972
- }, []);
973
- return null;
974
- };
975
- adminSdk.defineWidgetConfig({
976
- zone: "product_category.details.side.before"
977
- });
978
- const HideDefaultMetadataWidget = () => {
979
- react.useEffect(() => {
980
- const hideMetadataSection = () => {
981
- const headings = document.querySelectorAll("h2");
982
- headings.forEach((heading) => {
983
- var _a;
984
- if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
985
- let container = heading.parentElement;
986
- while (container && container !== document.body) {
987
- const hasContainerClass = container.classList.toString().includes("Container");
988
- const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
989
- if (hasContainerClass || isInSidebar) {
990
- const editLink = container.querySelector('a[href*="metadata/edit"]');
991
- const badge = container.querySelector('div[class*="Badge"]');
992
- if (editLink && badge) {
993
- container.style.display = "none";
994
- container.setAttribute("data-metadata-hidden", "true");
995
- return;
996
- }
997
- }
998
- container = container.parentElement;
999
- }
1000
- }
1001
- });
1002
- };
1003
- const runHide = () => {
1004
- setTimeout(hideMetadataSection, 100);
1005
- };
1006
- runHide();
1007
- const observer = new MutationObserver(() => {
1008
- const alreadyHidden = document.querySelector('[data-metadata-hidden="true"]');
1009
- if (!alreadyHidden) {
1010
- runHide();
1011
- }
1012
- });
1013
- observer.observe(document.body, {
1014
- childList: true,
1015
- subtree: true
1016
- });
1017
- return () => {
1018
- observer.disconnect();
1019
- const hidden = document.querySelector('[data-metadata-hidden="true"]');
1020
- if (hidden) {
1021
- hidden.style.display = "";
1022
- hidden.removeAttribute("data-metadata-hidden");
1023
- }
1024
- };
1025
- }, []);
1026
- return null;
1027
- };
1028
- adminSdk.defineWidgetConfig({
1029
- zone: "product.details.side.before"
1030
- });
1031
- const CONFIG_DOCS_URL$1 = "https://docs.medusajs.com/admin/extension-points/widgets#order-details";
1032
- const OrderMetadataTableWidget = ({ data }) => {
1033
- const { data: descriptors = [], isPending, isError } = useOrderMetadataConfig();
1034
- const orderId = (data == null ? void 0 : data.id) ?? void 0;
1035
- const [baselineMetadata, setBaselineMetadata] = react.useState(
1036
- (data == null ? void 0 : data.metadata) ?? {}
1037
- );
1038
- const queryClient = reactQuery.useQueryClient();
1039
- const previousOrderIdRef = react.useRef(orderId);
1040
- const isInitializedRef = react.useRef(false);
1041
- const dataRef = react.useRef(data);
1042
- const descriptorsRef = react.useRef(descriptors);
1043
- react.useEffect(() => {
1044
- dataRef.current = data;
1045
- descriptorsRef.current = descriptors;
1046
- }, [data, descriptors]);
1047
- react.useEffect(() => {
1048
- var _a;
1049
- if (previousOrderIdRef.current === orderId && isInitializedRef.current) {
1050
- return;
1051
- }
1052
- const orderIdChanged = previousOrderIdRef.current !== orderId;
1053
- if (orderIdChanged || !isInitializedRef.current) {
1054
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? ((_a = dataRef.current) == null ? void 0 : _a.metadata) ?? {};
1055
- const currentDescriptors = descriptorsRef.current.length > 0 ? descriptorsRef.current : descriptors;
1056
- if (currentDescriptors.length === 0) {
1057
- return;
1058
- }
1059
- previousOrderIdRef.current = orderId;
1060
- setBaselineMetadata(currentMetadata);
1061
- const newInitialState = buildInitialFormState(currentDescriptors, currentMetadata);
1062
- setValues(newInitialState);
1063
- isInitializedRef.current = true;
1064
- }
1065
- }, [orderId]);
1066
- const metadataStringRef = react.useRef("");
1067
- react.useEffect(() => {
1068
- const hasOrderData = !!data;
1069
- const descriptorsLoaded = descriptors.length > 0;
1070
- const sameOrder = previousOrderIdRef.current === orderId;
1071
- const notInitialized = !isInitializedRef.current;
1072
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? {};
1073
- const currentMetadataString = JSON.stringify(currentMetadata);
1074
- const metadataChanged = currentMetadataString !== metadataStringRef.current;
1075
- if (hasOrderData && descriptorsLoaded && sameOrder && (notInitialized || metadataChanged)) {
1076
- const newInitialState = buildInitialFormState(descriptors, currentMetadata);
1077
- setBaselineMetadata(currentMetadata);
1078
- setValues(newInitialState);
1079
- metadataStringRef.current = currentMetadataString;
1080
- isInitializedRef.current = true;
1081
- }
1082
- }, [data, descriptors.length, orderId]);
1083
- const initialState = react.useMemo(
1084
- () => buildInitialFormState(descriptors, baselineMetadata),
1085
- [descriptors, baselineMetadata]
1086
- );
1087
- const [values, setValues] = react.useState({});
1088
- const [isSaving, setIsSaving] = react.useState(false);
1089
- const errors = react.useMemo(() => {
1090
- return descriptors.reduce((acc, descriptor) => {
1091
- const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
1092
- if (error) {
1093
- acc[descriptor.key] = error;
1094
- }
1095
- return acc;
1096
- }, {});
1097
- }, [descriptors, values]);
1098
- const hasErrors = Object.keys(errors).length > 0;
1099
- const isDirty = react.useMemo(() => {
1100
- return hasMetadataChanges({
1101
- descriptors,
1102
- values,
1103
- originalMetadata: baselineMetadata
1104
- });
1105
- }, [descriptors, values, baselineMetadata]);
1106
- const handleStringChange = (key, nextValue) => {
1107
- setValues((prev) => ({
1108
- ...prev,
1109
- [key]: nextValue
1110
- }));
1111
- };
1112
- const handleBooleanChange = (key, nextValue) => {
1113
- setValues((prev) => ({
1114
- ...prev,
1115
- [key]: nextValue
1116
- }));
1117
- };
1118
- const handleReset = () => {
1119
- setValues(initialState);
1120
- };
1121
- const handleSubmit = async () => {
1122
- if (!(data == null ? void 0 : data.id) || !descriptors.length) {
1123
- return;
1124
- }
1125
- setIsSaving(true);
1126
- try {
1127
- const metadataPayload = buildMetadataPayload({
1128
- descriptors,
1129
- values,
1130
- originalMetadata: baselineMetadata
1131
- });
1132
- const response = await fetch(`/admin/orders/${data.id}`, {
1133
- method: "POST",
1134
- credentials: "include",
1135
- headers: {
1136
- "Content-Type": "application/json"
1137
- },
1138
- body: JSON.stringify({
1139
- metadata: metadataPayload
1140
- })
1141
- });
1142
- if (!response.ok) {
1143
- const payload = await response.json().catch(() => null);
1144
- throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
1145
- }
1146
- const updated = await response.json();
1147
- const nextMetadata = updated.order.metadata;
1148
- setBaselineMetadata(nextMetadata);
1149
- setValues(buildInitialFormState(descriptors, nextMetadata));
1150
- ui.toast.success("Metadata saved");
1151
- await queryClient.invalidateQueries({
1152
- queryKey: ["orders"]
1153
- });
1154
- await queryClient.invalidateQueries({
1155
- queryKey: ["order", data.id]
1156
- });
1157
- if (data.id) {
1158
- queryClient.refetchQueries({
1159
- queryKey: ["order", data.id]
1160
- }).catch(() => {
1161
- });
1162
- }
1163
- } catch (error) {
1164
- ui.toast.error(error instanceof Error ? error.message : "Save failed");
1165
- } finally {
1166
- setIsSaving(false);
1167
- }
1168
- };
1169
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
1170
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
1171
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
1172
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
1173
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
1174
- ] }),
1175
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
1176
- ] }),
1177
- isPending || !isInitializedRef.current || Object.keys(values).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
1178
- "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
1179
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
1180
- "."
1181
- ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
1182
- "Provide a ",
1183
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
1184
- " array in the plugin options to control which keys show up here.",
1185
- " ",
1186
- /* @__PURE__ */ jsxRuntime.jsx(
1187
- "a",
1188
- {
1189
- className: "text-ui-fg-interactive underline",
1190
- href: CONFIG_DOCS_URL$1,
1191
- target: "_blank",
1192
- rel: "noreferrer",
1193
- children: "Learn how to configure it."
1194
- }
1195
- )
1196
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1197
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1198
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1199
- /* @__PURE__ */ jsxRuntime.jsx(
1200
- "th",
1201
- {
1202
- scope: "col",
1203
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
1204
- children: "Label"
1205
- }
1206
- ),
1207
- /* @__PURE__ */ jsxRuntime.jsx(
1208
- "th",
1209
- {
1210
- scope: "col",
1211
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
1212
- children: "Value"
1213
- }
1214
- )
1215
- ] }) }),
1216
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
1217
- const value = values[descriptor.key];
1218
- const error = errors[descriptor.key];
1219
- return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1220
- /* @__PURE__ */ jsxRuntime.jsx(
1221
- "th",
1222
- {
1223
- scope: "row",
1224
- className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
1225
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1226
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
1227
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
1228
- ] })
1229
- }
1230
- ),
1231
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1232
- /* @__PURE__ */ jsxRuntime.jsx(
1233
- ValueField$1,
1234
- {
1235
- descriptor,
1236
- value,
1237
- onStringChange: handleStringChange,
1238
- onBooleanChange: handleBooleanChange
1239
- }
1240
- ),
1241
- error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
1242
- ] }) })
1243
- ] }, descriptor.key);
1244
- }) })
1245
- ] }) }),
1246
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 border-t border-ui-border-subtle pt-3 md:flex-row md:items-center md:justify-between", children: [
1247
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Changes are stored on the order metadata object. Clearing a field removes the corresponding key on save." }),
1248
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1249
- /* @__PURE__ */ jsxRuntime.jsx(
1250
- ui.Button,
1251
- {
1252
- variant: "secondary",
1253
- size: "small",
1254
- disabled: !isDirty || isSaving,
1255
- onClick: handleReset,
1256
- children: "Reset"
1257
- }
1258
- ),
1259
- /* @__PURE__ */ jsxRuntime.jsx(
1260
- ui.Button,
1261
- {
1262
- size: "small",
1263
- onClick: handleSubmit,
1264
- disabled: !isDirty || hasErrors || isSaving,
1265
- isLoading: isSaving,
1266
- children: "Save metadata"
1267
- }
1268
- )
1269
- ] })
1270
- ] })
1271
- ] })
1272
- ] });
1273
- };
1274
- const ValueField$1 = ({
1275
- descriptor,
1276
- value,
1277
- onStringChange,
1278
- onBooleanChange
1279
- }) => {
1280
- const fileInputRef = react.useRef(null);
1281
- const [isUploading, setIsUploading] = react.useState(false);
1282
- const handleFileUpload = async (event) => {
1283
- var _a;
1284
- const file = (_a = event.target.files) == null ? void 0 : _a[0];
1285
- if (!file) {
1286
- return;
1287
- }
1288
- setIsUploading(true);
1289
- try {
1290
- const formData = new FormData();
1291
- formData.append("files", file);
1292
- const response = await fetch("/admin/uploads", {
1293
- method: "POST",
1294
- credentials: "include",
1295
- body: formData
1296
- });
1297
- if (!response.ok) {
1298
- const payload = await response.json().catch(() => null);
1299
- throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
1300
- }
1301
- const result = await response.json();
1302
- if (result.files && result.files.length > 0) {
1303
- const uploadedFile = result.files[0];
1304
- const fileUrl = uploadedFile.url || uploadedFile.key;
1305
- if (fileUrl) {
1306
- onStringChange(descriptor.key, fileUrl);
1307
- ui.toast.success("File uploaded successfully");
1308
- } else {
1309
- throw new Error("File upload succeeded but no URL returned");
1310
- }
1311
- } else {
1312
- throw new Error("File upload failed - no files returned");
1313
- }
1314
- } catch (error) {
1315
- ui.toast.error(
1316
- error instanceof Error ? error.message : "Failed to upload file"
1317
- );
1318
- } finally {
1319
- setIsUploading(false);
1320
- if (fileInputRef.current) {
1321
- fileInputRef.current.value = "";
1322
- }
1323
- }
1324
- };
1325
- if (descriptor.type === "bool") {
1326
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1327
- /* @__PURE__ */ jsxRuntime.jsx(
1328
- ui.Switch,
1329
- {
1330
- checked: Boolean(value),
1331
- onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
1332
- "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
1333
- }
1334
- ),
1335
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
1336
- ] });
1337
- }
1338
- if (descriptor.type === "text") {
1339
- return /* @__PURE__ */ jsxRuntime.jsx(
1340
- ui.Textarea,
1341
- {
1342
- value: value ?? "",
1343
- placeholder: "Enter text",
1344
- rows: 3,
1345
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
1346
- }
1347
- );
1348
- }
1349
- if (descriptor.type === "number") {
1350
- return /* @__PURE__ */ jsxRuntime.jsx(
1351
- ui.Input,
1352
- {
1353
- type: "text",
1354
- inputMode: "decimal",
1355
- placeholder: "0.00",
1356
- value: value ?? "",
1357
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
1358
- }
1359
- );
1360
- }
1361
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1362
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1363
- /* @__PURE__ */ jsxRuntime.jsx(
1364
- ui.Input,
1365
- {
1366
- type: "url",
1367
- placeholder: "https://example.com/file",
1368
- value: value ?? "",
1369
- onChange: (event) => onStringChange(descriptor.key, event.target.value),
1370
- className: "flex-1"
1371
- }
1372
- ),
1373
- /* @__PURE__ */ jsxRuntime.jsx(
1374
- "input",
1375
- {
1376
- ref: fileInputRef,
1377
- type: "file",
1378
- className: "hidden",
1379
- onChange: handleFileUpload,
1380
- disabled: isUploading,
1381
- "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
1382
- }
1383
- ),
1384
- /* @__PURE__ */ jsxRuntime.jsx(
1385
- ui.Button,
1386
- {
1387
- type: "button",
1388
- variant: "secondary",
1389
- size: "small",
1390
- onClick: () => {
1391
- var _a;
1392
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1393
- },
1394
- disabled: isUploading,
1395
- isLoading: isUploading,
1396
- children: isUploading ? "Uploading..." : "Upload"
1397
- }
1398
- )
1399
- ] }),
1400
- typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
1401
- "a",
1402
- {
1403
- className: "txt-compact-small-plus text-ui-fg-interactive underline",
1404
- href: value,
1405
- target: "_blank",
1406
- rel: "noreferrer",
1407
- children: "View file"
1408
- }
1409
- )
1410
- ] });
1411
- };
1412
- adminSdk.defineWidgetConfig({
1413
- zone: "order.details.after"
1414
- });
1415
- const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
1416
- const ProductMetadataTableWidget = ({ data }) => {
1417
- const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
1418
- const productId = (data == null ? void 0 : data.id) ?? void 0;
1419
- const [baselineMetadata, setBaselineMetadata] = react.useState(
1420
- (data == null ? void 0 : data.metadata) ?? {}
1421
- );
1422
- const queryClient = reactQuery.useQueryClient();
1423
- const previousProductIdRef = react.useRef(productId);
1424
- const isInitializedRef = react.useRef(false);
1425
- const dataRef = react.useRef(data);
1426
- const descriptorsRef = react.useRef(descriptors);
1427
- react.useEffect(() => {
1428
- dataRef.current = data;
1429
- descriptorsRef.current = descriptors;
1430
- }, [data, descriptors]);
1431
- react.useEffect(() => {
1432
- var _a;
1433
- if (previousProductIdRef.current === productId && isInitializedRef.current) {
1434
- return;
1435
- }
1436
- const productIdChanged = previousProductIdRef.current !== productId;
1437
- if (productIdChanged || !isInitializedRef.current) {
1438
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? ((_a = dataRef.current) == null ? void 0 : _a.metadata) ?? {};
1439
- const currentDescriptors = descriptorsRef.current.length > 0 ? descriptorsRef.current : descriptors;
1440
- if (currentDescriptors.length === 0) {
1441
- return;
1442
- }
1443
- previousProductIdRef.current = productId;
1444
- setBaselineMetadata(currentMetadata);
1445
- const newInitialState = buildInitialFormState(currentDescriptors, currentMetadata);
1446
- setValues(newInitialState);
1447
- isInitializedRef.current = true;
1448
- }
1449
- }, [productId]);
1450
- const metadataStringRef = react.useRef("");
1451
- react.useEffect(() => {
1452
- const hasProductData = !!data;
1453
- const descriptorsLoaded = descriptors.length > 0;
1454
- const sameProduct = previousProductIdRef.current === productId;
1455
- const notInitialized = !isInitializedRef.current;
1456
- const currentMetadata = (data == null ? void 0 : data.metadata) ?? {};
1457
- const currentMetadataString = JSON.stringify(currentMetadata);
1458
- const metadataChanged = currentMetadataString !== metadataStringRef.current;
1459
- if (hasProductData && descriptorsLoaded && sameProduct && (notInitialized || metadataChanged)) {
1460
- const newInitialState = buildInitialFormState(descriptors, currentMetadata);
1461
- setBaselineMetadata(currentMetadata);
1462
- setValues(newInitialState);
1463
- metadataStringRef.current = currentMetadataString;
1464
- isInitializedRef.current = true;
1465
- }
1466
- }, [data, descriptors.length, productId]);
1467
- const initialState = react.useMemo(
1468
- () => buildInitialFormState(descriptors, baselineMetadata),
1469
- [descriptors, baselineMetadata]
1470
- );
1471
- const [values, setValues] = react.useState({});
1472
- const [isSaving, setIsSaving] = react.useState(false);
1473
- const errors = react.useMemo(() => {
1474
- return descriptors.reduce((acc, descriptor) => {
1475
- const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
1476
- if (error) {
1477
- acc[descriptor.key] = error;
1478
- }
1479
- return acc;
1480
- }, {});
1481
- }, [descriptors, values]);
1482
- const hasErrors = Object.keys(errors).length > 0;
1483
- const isDirty = react.useMemo(() => {
1484
- return hasMetadataChanges({
1485
- descriptors,
1486
- values,
1487
- originalMetadata: baselineMetadata
1488
- });
1489
- }, [descriptors, values, baselineMetadata]);
1490
- const handleStringChange = (key, nextValue) => {
1491
- setValues((prev) => ({
1492
- ...prev,
1493
- [key]: nextValue
1494
- }));
1495
- };
1496
- const handleBooleanChange = (key, nextValue) => {
1497
- setValues((prev) => ({
1498
- ...prev,
1499
- [key]: nextValue
1500
- }));
1501
- };
1502
- const handleReset = () => {
1503
- setValues(initialState);
1504
- };
1505
- const handleSubmit = async () => {
1506
- if (!(data == null ? void 0 : data.id) || !descriptors.length) {
1507
- return;
1508
- }
1509
- setIsSaving(true);
1510
- try {
1511
- const metadataPayload = buildMetadataPayload({
1512
- descriptors,
1513
- values,
1514
- originalMetadata: baselineMetadata
1515
- });
1516
- const response = await fetch(`/admin/products/${data.id}`, {
1517
- method: "POST",
1518
- credentials: "include",
1519
- headers: {
1520
- "Content-Type": "application/json"
1521
- },
1522
- body: JSON.stringify({
1523
- metadata: metadataPayload
1524
- })
1525
- });
1526
- if (!response.ok) {
1527
- const payload = await response.json().catch(() => null);
1528
- throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
1529
- }
1530
- const updated = await response.json();
1531
- const nextMetadata = updated.product.metadata;
1532
- setBaselineMetadata(nextMetadata);
1533
- setValues(buildInitialFormState(descriptors, nextMetadata));
1534
- ui.toast.success("Metadata saved");
1535
- await queryClient.invalidateQueries({
1536
- queryKey: ["products"]
1537
- });
1538
- await queryClient.invalidateQueries({
1539
- queryKey: ["product", data.id]
1540
- });
1541
- if (data.id) {
1542
- queryClient.refetchQueries({
1543
- queryKey: ["product", data.id]
1544
- }).catch(() => {
1545
- });
1546
- }
1547
- } catch (error) {
1548
- ui.toast.error(error instanceof Error ? error.message : "Save failed");
1549
- } finally {
1550
- setIsSaving(false);
1551
- }
1552
- };
1553
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
1554
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
1555
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
1556
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
1557
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
1558
- ] }),
1559
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
1560
- ] }),
1561
- isPending || !isInitializedRef.current || Object.keys(values).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
1562
- "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
1563
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
1564
- "."
1565
- ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
1566
- "Provide a ",
1567
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
1568
- " array in the plugin options to control which keys show up here.",
1569
- " ",
1570
- /* @__PURE__ */ jsxRuntime.jsx(
1571
- "a",
1572
- {
1573
- className: "text-ui-fg-interactive underline",
1574
- href: CONFIG_DOCS_URL,
1575
- target: "_blank",
1576
- rel: "noreferrer",
1577
- children: "Learn how to configure it."
1578
- }
1579
- )
1580
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1581
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1582
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1583
- /* @__PURE__ */ jsxRuntime.jsx(
1584
- "th",
1585
- {
1586
- scope: "col",
1587
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
1588
- children: "Label"
1589
- }
1590
- ),
1591
- /* @__PURE__ */ jsxRuntime.jsx(
1592
- "th",
1593
- {
1594
- scope: "col",
1595
- className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
1596
- children: "Value"
1597
- }
1598
- )
1599
- ] }) }),
1600
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
1601
- const value = values[descriptor.key];
1602
- const error = errors[descriptor.key];
1603
- return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1604
- /* @__PURE__ */ jsxRuntime.jsx(
1605
- "th",
1606
- {
1607
- scope: "row",
1608
- className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
1609
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1610
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
1611
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
1612
- ] })
1613
- }
1614
- ),
1615
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1616
- /* @__PURE__ */ jsxRuntime.jsx(
1617
- ValueField,
1618
- {
1619
- descriptor,
1620
- value,
1621
- onStringChange: handleStringChange,
1622
- onBooleanChange: handleBooleanChange
1623
- }
1624
- ),
1625
- error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
1626
- ] }) })
1627
- ] }, descriptor.key);
1628
- }) })
1629
- ] }) }),
1630
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 border-t border-ui-border-subtle pt-3 md:flex-row md:items-center md:justify-between", children: [
1631
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Changes are stored on the product metadata object. Clearing a field removes the corresponding key on save." }),
1632
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1633
- /* @__PURE__ */ jsxRuntime.jsx(
1634
- ui.Button,
1635
- {
1636
- variant: "secondary",
1637
- size: "small",
1638
- disabled: !isDirty || isSaving,
1639
- onClick: handleReset,
1640
- children: "Reset"
1641
- }
1642
- ),
1643
- /* @__PURE__ */ jsxRuntime.jsx(
1644
- ui.Button,
1645
- {
1646
- size: "small",
1647
- onClick: handleSubmit,
1648
- disabled: !isDirty || hasErrors || isSaving,
1649
- isLoading: isSaving,
1650
- children: "Save metadata"
1651
- }
1652
- )
1653
- ] })
1654
- ] })
1655
- ] })
1656
- ] });
1657
- };
1658
- const ValueField = ({
1659
- descriptor,
1660
- value,
1661
- onStringChange,
1662
- onBooleanChange
1663
- }) => {
1664
- const fileInputRef = react.useRef(null);
1665
- const [isUploading, setIsUploading] = react.useState(false);
1666
- const handleFileUpload = async (event) => {
1667
- var _a;
1668
- const file = (_a = event.target.files) == null ? void 0 : _a[0];
1669
- if (!file) {
1670
- return;
1671
- }
1672
- setIsUploading(true);
1673
- try {
1674
- const formData = new FormData();
1675
- formData.append("files", file);
1676
- const response = await fetch("/admin/uploads", {
1677
- method: "POST",
1678
- credentials: "include",
1679
- body: formData
1680
- });
1681
- if (!response.ok) {
1682
- const payload = await response.json().catch(() => null);
1683
- throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
1684
- }
1685
- const result = await response.json();
1686
- if (result.files && result.files.length > 0) {
1687
- const uploadedFile = result.files[0];
1688
- const fileUrl = uploadedFile.url || uploadedFile.key;
1689
- if (fileUrl) {
1690
- onStringChange(descriptor.key, fileUrl);
1691
- ui.toast.success("File uploaded successfully");
1692
- } else {
1693
- throw new Error("File upload succeeded but no URL returned");
1694
- }
1695
- } else {
1696
- throw new Error("File upload failed - no files returned");
1697
- }
1698
- } catch (error) {
1699
- ui.toast.error(
1700
- error instanceof Error ? error.message : "Failed to upload file"
1701
- );
1702
- } finally {
1703
- setIsUploading(false);
1704
- if (fileInputRef.current) {
1705
- fileInputRef.current.value = "";
1706
- }
1707
- }
1708
- };
1709
- if (descriptor.type === "bool") {
1710
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1711
- /* @__PURE__ */ jsxRuntime.jsx(
1712
- ui.Switch,
1713
- {
1714
- checked: Boolean(value),
1715
- onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
1716
- "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
1717
- }
1718
- ),
1719
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
1720
- ] });
1721
- }
1722
- if (descriptor.type === "text") {
1723
- return /* @__PURE__ */ jsxRuntime.jsx(
1724
- ui.Textarea,
1725
- {
1726
- value: value ?? "",
1727
- placeholder: "Enter text",
1728
- rows: 3,
1729
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
1730
- }
1731
- );
1732
- }
1733
- if (descriptor.type === "number") {
1734
- return /* @__PURE__ */ jsxRuntime.jsx(
1735
- ui.Input,
1736
- {
1737
- type: "text",
1738
- inputMode: "decimal",
1739
- placeholder: "0.00",
1740
- value: value ?? "",
1741
- onChange: (event) => onStringChange(descriptor.key, event.target.value)
1742
- }
1743
- );
1744
- }
1745
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
1746
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1747
- /* @__PURE__ */ jsxRuntime.jsx(
1748
- ui.Input,
1749
- {
1750
- type: "url",
1751
- placeholder: "https://example.com/file",
1752
- value: value ?? "",
1753
- onChange: (event) => onStringChange(descriptor.key, event.target.value),
1754
- className: "flex-1"
1755
- }
1756
- ),
1757
- /* @__PURE__ */ jsxRuntime.jsx(
1758
- "input",
1759
- {
1760
- ref: fileInputRef,
1761
- type: "file",
1762
- className: "hidden",
1763
- onChange: handleFileUpload,
1764
- disabled: isUploading,
1765
- "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
1766
- }
1767
- ),
1768
- /* @__PURE__ */ jsxRuntime.jsx(
1769
- ui.Button,
1770
- {
1771
- type: "button",
1772
- variant: "secondary",
1773
- size: "small",
1774
- onClick: () => {
1775
- var _a;
1776
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1777
- },
1778
- disabled: isUploading,
1779
- isLoading: isUploading,
1780
- children: isUploading ? "Uploading..." : "Upload"
1781
- }
1782
- )
1783
- ] }),
1784
- typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
1785
- "a",
1786
- {
1787
- className: "txt-compact-small-plus text-ui-fg-interactive underline",
1788
- href: value,
1789
- target: "_blank",
1790
- rel: "noreferrer",
1791
- children: "View file"
1792
- }
1793
- )
1794
- ] });
1795
- };
1796
- adminSdk.defineWidgetConfig({
1797
- zone: "product.details.after"
1798
- });
5
+ const adminSdk = require("@medusajs/admin-sdk");
6
+ const ui = require("@medusajs/ui");
1799
7
  const fetchJson = async (path) => {
1800
8
  const response = await fetch(path, {
1801
9
  credentials: "include"
@@ -1921,30 +129,6 @@ adminSdk.defineWidgetConfig({
1921
129
  });
1922
130
  const i18nTranslations0 = {};
1923
131
  const widgetModule = { widgets: [
1924
- {
1925
- Component: CategoryMetadataTableWidget,
1926
- zone: ["product_category.details.after"]
1927
- },
1928
- {
1929
- Component: CollectionMetadataTableWidget,
1930
- zone: ["product_collection.details.after"]
1931
- },
1932
- {
1933
- Component: HideCategoryDefaultMetadataWidget,
1934
- zone: ["product_category.details.side.before"]
1935
- },
1936
- {
1937
- Component: HideDefaultMetadataWidget,
1938
- zone: ["product.details.side.before"]
1939
- },
1940
- {
1941
- Component: OrderMetadataTableWidget,
1942
- zone: ["order.details.after"]
1943
- },
1944
- {
1945
- Component: ProductMetadataTableWidget,
1946
- zone: ["product.details.after"]
1947
- },
1948
132
  {
1949
133
  Component: ProductWishlistStatsWidget,
1950
134
  zone: ["product.details.side.after"]