medusa-product-helper 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,9 @@
1
1
  "use strict";
2
- const adminSdk = require("@medusajs/admin-sdk");
3
- const react = require("react");
4
2
  const jsxRuntime = require("react/jsx-runtime");
3
+ const adminSdk = require("@medusajs/admin-sdk");
5
4
  const ui = require("@medusajs/ui");
5
+ const react = require("react");
6
6
  const reactQuery = require("@tanstack/react-query");
7
- const HideDefaultMetadataWidget = () => {
8
- react.useEffect(() => {
9
- const hideMetadataSection = () => {
10
- const headings = document.querySelectorAll("h2");
11
- headings.forEach((heading) => {
12
- var _a;
13
- if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
14
- let container = heading.parentElement;
15
- while (container && container !== document.body) {
16
- const hasContainerClass = container.classList.toString().includes("Container");
17
- const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
18
- if (hasContainerClass || isInSidebar) {
19
- const editLink = container.querySelector('a[href*="metadata/edit"]');
20
- const badge = container.querySelector('div[class*="Badge"]');
21
- if (editLink && badge) {
22
- container.style.display = "none";
23
- container.setAttribute("data-metadata-hidden", "true");
24
- return;
25
- }
26
- }
27
- container = container.parentElement;
28
- }
29
- }
30
- });
31
- };
32
- const runHide = () => {
33
- setTimeout(hideMetadataSection, 100);
34
- };
35
- runHide();
36
- const observer = new MutationObserver(() => {
37
- const alreadyHidden = document.querySelector('[data-metadata-hidden="true"]');
38
- if (!alreadyHidden) {
39
- runHide();
40
- }
41
- });
42
- observer.observe(document.body, {
43
- childList: true,
44
- subtree: true
45
- });
46
- return () => {
47
- observer.disconnect();
48
- const hidden = document.querySelector('[data-metadata-hidden="true"]');
49
- if (hidden) {
50
- hidden.style.display = "";
51
- hidden.removeAttribute("data-metadata-hidden");
52
- }
53
- };
54
- }, []);
55
- return null;
56
- };
57
- adminSdk.defineWidgetConfig({
58
- zone: "product.details.side.before"
59
- });
60
7
  const METADATA_FIELD_TYPES = ["number", "text", "file", "bool"];
61
8
  const VALID_FIELD_TYPES = new Set(METADATA_FIELD_TYPES);
62
9
  function normalizeMetadataDescriptors(input) {
@@ -78,10 +25,12 @@ function normalizeMetadataDescriptors(input) {
78
25
  continue;
79
26
  }
80
27
  const label = getNormalizedLabel(item.label);
28
+ const filterable = typeof item.filterable === "boolean" ? item.filterable : Boolean(item.filterable);
81
29
  normalized.push({
82
30
  key,
83
31
  type,
84
- ...label ? { label } : {}
32
+ ...label ? { label } : {},
33
+ ...filterable ? { filterable: true } : {}
85
34
  });
86
35
  seenKeys.add(key);
87
36
  }
@@ -237,11 +186,11 @@ function isDeepEqual(a, b) {
237
186
  }
238
187
  const CONFIG_ENDPOINT = "/admin/product-metadata-config";
239
188
  const QUERY_KEY = ["medusa-product-helper", "metadata-config"];
240
- const useProductMetadataConfig = () => {
189
+ const useMetadataConfig = (entity) => {
241
190
  return reactQuery.useQuery({
242
- queryKey: QUERY_KEY,
191
+ queryKey: [...QUERY_KEY, entity],
243
192
  queryFn: async () => {
244
- const response = await fetch(CONFIG_ENDPOINT, {
193
+ const response = await fetch(`${CONFIG_ENDPOINT}?entity=${entity}`, {
245
194
  credentials: "include"
246
195
  });
247
196
  if (!response.ok) {
@@ -253,6 +202,455 @@ const useProductMetadataConfig = () => {
253
202
  staleTime: 5 * 60 * 1e3
254
203
  });
255
204
  };
205
+ const useProductMetadataConfig = () => useMetadataConfig("product");
206
+ const useCategoryMetadataConfig = () => useMetadataConfig("category");
207
+ const CONFIG_DOCS_URL$1 = "https://docs.medusajs.com/admin/extension-points/widgets#product-category-details";
208
+ const CategoryMetadataTableWidget = ({ data }) => {
209
+ const { data: descriptors = [], isPending, isError } = useCategoryMetadataConfig();
210
+ const metadata = (data == null ? void 0 : data.metadata) ?? {};
211
+ const [baselineMetadata, setBaselineMetadata] = react.useState(metadata);
212
+ const queryClient = reactQuery.useQueryClient();
213
+ react.useEffect(() => {
214
+ setBaselineMetadata(metadata);
215
+ }, [metadata]);
216
+ const initialState = react.useMemo(
217
+ () => buildInitialFormState(descriptors, baselineMetadata),
218
+ [descriptors, baselineMetadata]
219
+ );
220
+ const [values, setValues] = react.useState(
221
+ initialState
222
+ );
223
+ const [isSaving, setIsSaving] = react.useState(false);
224
+ react.useEffect(() => {
225
+ setValues(initialState);
226
+ }, [initialState]);
227
+ const errors = react.useMemo(() => {
228
+ return descriptors.reduce((acc, descriptor) => {
229
+ const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
230
+ if (error) {
231
+ acc[descriptor.key] = error;
232
+ }
233
+ return acc;
234
+ }, {});
235
+ }, [descriptors, values]);
236
+ const hasErrors = Object.keys(errors).length > 0;
237
+ const isDirty = react.useMemo(() => {
238
+ return hasMetadataChanges({
239
+ descriptors,
240
+ values,
241
+ originalMetadata: baselineMetadata
242
+ });
243
+ }, [descriptors, values, baselineMetadata]);
244
+ const handleStringChange = (key, nextValue) => {
245
+ setValues((prev) => ({
246
+ ...prev,
247
+ [key]: nextValue
248
+ }));
249
+ };
250
+ const handleBooleanChange = (key, nextValue) => {
251
+ setValues((prev) => ({
252
+ ...prev,
253
+ [key]: nextValue
254
+ }));
255
+ };
256
+ const handleReset = () => {
257
+ setValues(initialState);
258
+ };
259
+ const handleSubmit = async () => {
260
+ if (!(data == null ? void 0 : data.id) || !descriptors.length) {
261
+ return;
262
+ }
263
+ setIsSaving(true);
264
+ try {
265
+ const metadataPayload = buildMetadataPayload({
266
+ descriptors,
267
+ values,
268
+ originalMetadata: baselineMetadata
269
+ });
270
+ const response = await fetch(`/admin/product-categories/${data.id}`, {
271
+ method: "POST",
272
+ credentials: "include",
273
+ headers: {
274
+ "Content-Type": "application/json"
275
+ },
276
+ body: JSON.stringify({
277
+ metadata: metadataPayload
278
+ })
279
+ });
280
+ if (!response.ok) {
281
+ const payload = await response.json().catch(() => null);
282
+ throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
283
+ }
284
+ const updated = await response.json();
285
+ const nextMetadata = updated.product_category.metadata;
286
+ setBaselineMetadata(nextMetadata);
287
+ setValues(buildInitialFormState(descriptors, nextMetadata));
288
+ ui.toast.success("Metadata saved");
289
+ await queryClient.invalidateQueries({
290
+ queryKey: ["product-categories"]
291
+ });
292
+ } catch (error) {
293
+ ui.toast.error(error instanceof Error ? error.message : "Save failed");
294
+ } finally {
295
+ setIsSaving(false);
296
+ }
297
+ };
298
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
299
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
300
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
302
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
303
+ ] }),
304
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
305
+ ] }),
306
+ isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
307
+ "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
308
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
309
+ "."
310
+ ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
311
+ "Provide a ",
312
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
313
+ " array in the plugin options to control which keys show up here.",
314
+ " ",
315
+ /* @__PURE__ */ jsxRuntime.jsx(
316
+ "a",
317
+ {
318
+ className: "text-ui-fg-interactive underline",
319
+ href: CONFIG_DOCS_URL$1,
320
+ target: "_blank",
321
+ rel: "noreferrer",
322
+ children: "Learn how to configure it."
323
+ }
324
+ )
325
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
326
+ /* @__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: [
327
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
328
+ /* @__PURE__ */ jsxRuntime.jsx(
329
+ "th",
330
+ {
331
+ scope: "col",
332
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
333
+ children: "Label"
334
+ }
335
+ ),
336
+ /* @__PURE__ */ jsxRuntime.jsx(
337
+ "th",
338
+ {
339
+ scope: "col",
340
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
341
+ children: "Value"
342
+ }
343
+ )
344
+ ] }) }),
345
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
346
+ const value = values[descriptor.key];
347
+ const error = errors[descriptor.key];
348
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx(
350
+ "th",
351
+ {
352
+ scope: "row",
353
+ className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
354
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
355
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
357
+ ] })
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
361
+ /* @__PURE__ */ jsxRuntime.jsx(
362
+ ValueField$1,
363
+ {
364
+ descriptor,
365
+ value,
366
+ onStringChange: handleStringChange,
367
+ onBooleanChange: handleBooleanChange
368
+ }
369
+ ),
370
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
371
+ ] }) })
372
+ ] }, descriptor.key);
373
+ }) })
374
+ ] }) }),
375
+ /* @__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: [
376
+ /* @__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." }),
377
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
378
+ /* @__PURE__ */ jsxRuntime.jsx(
379
+ ui.Button,
380
+ {
381
+ variant: "secondary",
382
+ size: "small",
383
+ disabled: !isDirty || isSaving,
384
+ onClick: handleReset,
385
+ children: "Reset"
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsxRuntime.jsx(
389
+ ui.Button,
390
+ {
391
+ size: "small",
392
+ onClick: handleSubmit,
393
+ disabled: !isDirty || hasErrors || isSaving,
394
+ isLoading: isSaving,
395
+ children: "Save metadata"
396
+ }
397
+ )
398
+ ] })
399
+ ] })
400
+ ] })
401
+ ] });
402
+ };
403
+ const ValueField$1 = ({
404
+ descriptor,
405
+ value,
406
+ onStringChange,
407
+ onBooleanChange
408
+ }) => {
409
+ const fileInputRef = react.useRef(null);
410
+ const [isUploading, setIsUploading] = react.useState(false);
411
+ const handleFileUpload = async (event) => {
412
+ var _a;
413
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
414
+ if (!file) {
415
+ return;
416
+ }
417
+ setIsUploading(true);
418
+ try {
419
+ const formData = new FormData();
420
+ formData.append("files", file);
421
+ const response = await fetch("/admin/uploads", {
422
+ method: "POST",
423
+ credentials: "include",
424
+ body: formData
425
+ });
426
+ if (!response.ok) {
427
+ const payload = await response.json().catch(() => null);
428
+ throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
429
+ }
430
+ const result = await response.json();
431
+ if (result.files && result.files.length > 0) {
432
+ const uploadedFile = result.files[0];
433
+ const fileUrl = uploadedFile.url || uploadedFile.key;
434
+ if (fileUrl) {
435
+ onStringChange(descriptor.key, fileUrl);
436
+ ui.toast.success("File uploaded successfully");
437
+ } else {
438
+ throw new Error("File upload succeeded but no URL returned");
439
+ }
440
+ } else {
441
+ throw new Error("File upload failed - no files returned");
442
+ }
443
+ } catch (error) {
444
+ ui.toast.error(
445
+ error instanceof Error ? error.message : "Failed to upload file"
446
+ );
447
+ } finally {
448
+ setIsUploading(false);
449
+ if (fileInputRef.current) {
450
+ fileInputRef.current.value = "";
451
+ }
452
+ }
453
+ };
454
+ if (descriptor.type === "bool") {
455
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
456
+ /* @__PURE__ */ jsxRuntime.jsx(
457
+ ui.Switch,
458
+ {
459
+ checked: Boolean(value),
460
+ onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
461
+ "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
462
+ }
463
+ ),
464
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
465
+ ] });
466
+ }
467
+ if (descriptor.type === "text") {
468
+ return /* @__PURE__ */ jsxRuntime.jsx(
469
+ ui.Textarea,
470
+ {
471
+ value: value ?? "",
472
+ placeholder: "Enter text",
473
+ rows: 3,
474
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
475
+ }
476
+ );
477
+ }
478
+ if (descriptor.type === "number") {
479
+ return /* @__PURE__ */ jsxRuntime.jsx(
480
+ ui.Input,
481
+ {
482
+ type: "text",
483
+ inputMode: "decimal",
484
+ placeholder: "0.00",
485
+ value: value ?? "",
486
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
487
+ }
488
+ );
489
+ }
490
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
491
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
492
+ /* @__PURE__ */ jsxRuntime.jsx(
493
+ ui.Input,
494
+ {
495
+ type: "url",
496
+ placeholder: "https://example.com/file",
497
+ value: value ?? "",
498
+ onChange: (event) => onStringChange(descriptor.key, event.target.value),
499
+ className: "flex-1"
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsxRuntime.jsx(
503
+ "input",
504
+ {
505
+ ref: fileInputRef,
506
+ type: "file",
507
+ className: "hidden",
508
+ onChange: handleFileUpload,
509
+ disabled: isUploading,
510
+ "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
511
+ }
512
+ ),
513
+ /* @__PURE__ */ jsxRuntime.jsx(
514
+ ui.Button,
515
+ {
516
+ type: "button",
517
+ variant: "secondary",
518
+ size: "small",
519
+ onClick: () => {
520
+ var _a;
521
+ return (_a = fileInputRef.current) == null ? void 0 : _a.click();
522
+ },
523
+ disabled: isUploading,
524
+ isLoading: isUploading,
525
+ children: isUploading ? "Uploading..." : "Upload"
526
+ }
527
+ )
528
+ ] }),
529
+ typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
530
+ "a",
531
+ {
532
+ className: "txt-compact-small-plus text-ui-fg-interactive underline",
533
+ href: value,
534
+ target: "_blank",
535
+ rel: "noreferrer",
536
+ children: "View file"
537
+ }
538
+ )
539
+ ] });
540
+ };
541
+ adminSdk.defineWidgetConfig({
542
+ zone: "product_category.details.after"
543
+ });
544
+ const HideCategoryDefaultMetadataWidget = () => {
545
+ react.useEffect(() => {
546
+ const hideMetadataSection = () => {
547
+ const headings = document.querySelectorAll("h2");
548
+ headings.forEach((heading) => {
549
+ var _a;
550
+ if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
551
+ let container = heading.parentElement;
552
+ while (container && container !== document.body) {
553
+ const hasContainerClass = container.classList.toString().includes("Container");
554
+ const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
555
+ if (hasContainerClass || isInSidebar) {
556
+ const editLink = container.querySelector('a[href*="metadata/edit"]');
557
+ const badge = container.querySelector('div[class*="Badge"]');
558
+ if (editLink && badge) {
559
+ container.style.display = "none";
560
+ container.setAttribute("data-category-metadata-hidden", "true");
561
+ return;
562
+ }
563
+ }
564
+ container = container.parentElement;
565
+ }
566
+ }
567
+ });
568
+ };
569
+ const runHide = () => {
570
+ setTimeout(hideMetadataSection, 100);
571
+ };
572
+ runHide();
573
+ const observer = new MutationObserver(() => {
574
+ const alreadyHidden = document.querySelector(
575
+ '[data-category-metadata-hidden="true"]'
576
+ );
577
+ if (!alreadyHidden) {
578
+ runHide();
579
+ }
580
+ });
581
+ observer.observe(document.body, {
582
+ childList: true,
583
+ subtree: true
584
+ });
585
+ return () => {
586
+ observer.disconnect();
587
+ const hidden = document.querySelector(
588
+ '[data-category-metadata-hidden="true"]'
589
+ );
590
+ if (hidden) {
591
+ hidden.style.display = "";
592
+ hidden.removeAttribute("data-category-metadata-hidden");
593
+ }
594
+ };
595
+ }, []);
596
+ return null;
597
+ };
598
+ adminSdk.defineWidgetConfig({
599
+ zone: "product_category.details.side.before"
600
+ });
601
+ const HideDefaultMetadataWidget = () => {
602
+ react.useEffect(() => {
603
+ const hideMetadataSection = () => {
604
+ const headings = document.querySelectorAll("h2");
605
+ headings.forEach((heading) => {
606
+ var _a;
607
+ if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
608
+ let container = heading.parentElement;
609
+ while (container && container !== document.body) {
610
+ const hasContainerClass = container.classList.toString().includes("Container");
611
+ const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
612
+ if (hasContainerClass || isInSidebar) {
613
+ const editLink = container.querySelector('a[href*="metadata/edit"]');
614
+ const badge = container.querySelector('div[class*="Badge"]');
615
+ if (editLink && badge) {
616
+ container.style.display = "none";
617
+ container.setAttribute("data-metadata-hidden", "true");
618
+ return;
619
+ }
620
+ }
621
+ container = container.parentElement;
622
+ }
623
+ }
624
+ });
625
+ };
626
+ const runHide = () => {
627
+ setTimeout(hideMetadataSection, 100);
628
+ };
629
+ runHide();
630
+ const observer = new MutationObserver(() => {
631
+ const alreadyHidden = document.querySelector('[data-metadata-hidden="true"]');
632
+ if (!alreadyHidden) {
633
+ runHide();
634
+ }
635
+ });
636
+ observer.observe(document.body, {
637
+ childList: true,
638
+ subtree: true
639
+ });
640
+ return () => {
641
+ observer.disconnect();
642
+ const hidden = document.querySelector('[data-metadata-hidden="true"]');
643
+ if (hidden) {
644
+ hidden.style.display = "";
645
+ hidden.removeAttribute("data-metadata-hidden");
646
+ }
647
+ };
648
+ }, []);
649
+ return null;
650
+ };
651
+ adminSdk.defineWidgetConfig({
652
+ zone: "product.details.side.before"
653
+ });
256
654
  const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
257
655
  const ProductMetadataTableWidget = ({ data }) => {
258
656
  const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
@@ -592,6 +990,14 @@ adminSdk.defineWidgetConfig({
592
990
  });
593
991
  const i18nTranslations0 = {};
594
992
  const widgetModule = { widgets: [
993
+ {
994
+ Component: CategoryMetadataTableWidget,
995
+ zone: ["product_category.details.after"]
996
+ },
997
+ {
998
+ Component: HideCategoryDefaultMetadataWidget,
999
+ zone: ["product_category.details.side.before"]
1000
+ },
595
1001
  {
596
1002
  Component: HideDefaultMetadataWidget,
597
1003
  zone: ["product.details.side.before"]