medusa-product-helper 0.0.4 → 0.0.7

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,9 +202,11 @@ const useProductMetadataConfig = () => {
253
202
  staleTime: 5 * 60 * 1e3
254
203
  });
255
204
  };
256
- const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
257
- const ProductMetadataTableWidget = ({ data }) => {
258
- const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
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();
259
210
  const metadata = (data == null ? void 0 : data.metadata) ?? {};
260
211
  const [baselineMetadata, setBaselineMetadata] = react.useState(metadata);
261
212
  const queryClient = reactQuery.useQueryClient();
@@ -316,7 +267,7 @@ const ProductMetadataTableWidget = ({ data }) => {
316
267
  values,
317
268
  originalMetadata: baselineMetadata
318
269
  });
319
- const response = await fetch(`/admin/products/${data.id}`, {
270
+ const response = await fetch(`/admin/product-categories/${data.id}`, {
320
271
  method: "POST",
321
272
  credentials: "include",
322
273
  headers: {
@@ -331,12 +282,12 @@ const ProductMetadataTableWidget = ({ data }) => {
331
282
  throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
332
283
  }
333
284
  const updated = await response.json();
334
- const nextMetadata = updated.product.metadata;
285
+ const nextMetadata = updated.product_category.metadata;
335
286
  setBaselineMetadata(nextMetadata);
336
287
  setValues(buildInitialFormState(descriptors, nextMetadata));
337
288
  ui.toast.success("Metadata saved");
338
289
  await queryClient.invalidateQueries({
339
- queryKey: ["products"]
290
+ queryKey: ["product-categories"]
340
291
  });
341
292
  } catch (error) {
342
293
  ui.toast.error(error instanceof Error ? error.message : "Save failed");
@@ -365,7 +316,7 @@ const ProductMetadataTableWidget = ({ data }) => {
365
316
  "a",
366
317
  {
367
318
  className: "text-ui-fg-interactive underline",
368
- href: CONFIG_DOCS_URL,
319
+ href: CONFIG_DOCS_URL$1,
369
320
  target: "_blank",
370
321
  rel: "noreferrer",
371
322
  children: "Learn how to configure it."
@@ -408,7 +359,7 @@ const ProductMetadataTableWidget = ({ data }) => {
408
359
  ),
409
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: [
410
361
  /* @__PURE__ */ jsxRuntime.jsx(
411
- ValueField,
362
+ ValueField$1,
412
363
  {
413
364
  descriptor,
414
365
  value,
@@ -422,7 +373,7 @@ const ProductMetadataTableWidget = ({ data }) => {
422
373
  }) })
423
374
  ] }) }),
424
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: [
425
- /* @__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." }),
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." }),
426
377
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
427
378
  /* @__PURE__ */ jsxRuntime.jsx(
428
379
  ui.Button,
@@ -449,7 +400,7 @@ const ProductMetadataTableWidget = ({ data }) => {
449
400
  ] })
450
401
  ] });
451
402
  };
452
- const ValueField = ({
403
+ const ValueField$1 = ({
453
404
  descriptor,
454
405
  value,
455
406
  onStringChange,
@@ -588,17 +539,599 @@ const ValueField = ({
588
539
  ] });
589
540
  };
590
541
  adminSdk.defineWidgetConfig({
591
- zone: "product.details.after"
542
+ zone: "product_category.details.after"
592
543
  });
593
- const i18nTranslations0 = {};
594
- const widgetModule = { widgets: [
595
- {
596
- Component: HideDefaultMetadataWidget,
597
- zone: ["product.details.side.before"]
598
- },
599
- {
600
- Component: ProductMetadataTableWidget,
601
- zone: ["product.details.after"]
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
+ });
654
+ const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
655
+ const ProductMetadataTableWidget = ({ data }) => {
656
+ const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
657
+ const metadata = (data == null ? void 0 : data.metadata) ?? {};
658
+ const [baselineMetadata, setBaselineMetadata] = react.useState(metadata);
659
+ const queryClient = reactQuery.useQueryClient();
660
+ react.useEffect(() => {
661
+ setBaselineMetadata(metadata);
662
+ }, [metadata]);
663
+ const initialState = react.useMemo(
664
+ () => buildInitialFormState(descriptors, baselineMetadata),
665
+ [descriptors, baselineMetadata]
666
+ );
667
+ const [values, setValues] = react.useState(
668
+ initialState
669
+ );
670
+ const [isSaving, setIsSaving] = react.useState(false);
671
+ react.useEffect(() => {
672
+ setValues(initialState);
673
+ }, [initialState]);
674
+ const errors = react.useMemo(() => {
675
+ return descriptors.reduce((acc, descriptor) => {
676
+ const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
677
+ if (error) {
678
+ acc[descriptor.key] = error;
679
+ }
680
+ return acc;
681
+ }, {});
682
+ }, [descriptors, values]);
683
+ const hasErrors = Object.keys(errors).length > 0;
684
+ const isDirty = react.useMemo(() => {
685
+ return hasMetadataChanges({
686
+ descriptors,
687
+ values,
688
+ originalMetadata: baselineMetadata
689
+ });
690
+ }, [descriptors, values, baselineMetadata]);
691
+ const handleStringChange = (key, nextValue) => {
692
+ setValues((prev) => ({
693
+ ...prev,
694
+ [key]: nextValue
695
+ }));
696
+ };
697
+ const handleBooleanChange = (key, nextValue) => {
698
+ setValues((prev) => ({
699
+ ...prev,
700
+ [key]: nextValue
701
+ }));
702
+ };
703
+ const handleReset = () => {
704
+ setValues(initialState);
705
+ };
706
+ const handleSubmit = async () => {
707
+ if (!(data == null ? void 0 : data.id) || !descriptors.length) {
708
+ return;
709
+ }
710
+ setIsSaving(true);
711
+ try {
712
+ const metadataPayload = buildMetadataPayload({
713
+ descriptors,
714
+ values,
715
+ originalMetadata: baselineMetadata
716
+ });
717
+ const response = await fetch(`/admin/products/${data.id}`, {
718
+ method: "POST",
719
+ credentials: "include",
720
+ headers: {
721
+ "Content-Type": "application/json"
722
+ },
723
+ body: JSON.stringify({
724
+ metadata: metadataPayload
725
+ })
726
+ });
727
+ if (!response.ok) {
728
+ const payload = await response.json().catch(() => null);
729
+ throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
730
+ }
731
+ const updated = await response.json();
732
+ const nextMetadata = updated.product.metadata;
733
+ setBaselineMetadata(nextMetadata);
734
+ setValues(buildInitialFormState(descriptors, nextMetadata));
735
+ ui.toast.success("Metadata saved");
736
+ await queryClient.invalidateQueries({
737
+ queryKey: ["products"]
738
+ });
739
+ } catch (error) {
740
+ ui.toast.error(error instanceof Error ? error.message : "Save failed");
741
+ } finally {
742
+ setIsSaving(false);
743
+ }
744
+ };
745
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
746
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
747
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
748
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
749
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
750
+ ] }),
751
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
752
+ ] }),
753
+ isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
754
+ "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
755
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
756
+ "."
757
+ ] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
758
+ "Provide a ",
759
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
760
+ " array in the plugin options to control which keys show up here.",
761
+ " ",
762
+ /* @__PURE__ */ jsxRuntime.jsx(
763
+ "a",
764
+ {
765
+ className: "text-ui-fg-interactive underline",
766
+ href: CONFIG_DOCS_URL,
767
+ target: "_blank",
768
+ rel: "noreferrer",
769
+ children: "Learn how to configure it."
770
+ }
771
+ )
772
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
773
+ /* @__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: [
774
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx(
776
+ "th",
777
+ {
778
+ scope: "col",
779
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
780
+ children: "Label"
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsxRuntime.jsx(
784
+ "th",
785
+ {
786
+ scope: "col",
787
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
788
+ children: "Value"
789
+ }
790
+ )
791
+ ] }) }),
792
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
793
+ const value = values[descriptor.key];
794
+ const error = errors[descriptor.key];
795
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
796
+ /* @__PURE__ */ jsxRuntime.jsx(
797
+ "th",
798
+ {
799
+ scope: "row",
800
+ className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
801
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
802
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
803
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
804
+ ] })
805
+ }
806
+ ),
807
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
808
+ /* @__PURE__ */ jsxRuntime.jsx(
809
+ ValueField,
810
+ {
811
+ descriptor,
812
+ value,
813
+ onStringChange: handleStringChange,
814
+ onBooleanChange: handleBooleanChange
815
+ }
816
+ ),
817
+ error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
818
+ ] }) })
819
+ ] }, descriptor.key);
820
+ }) })
821
+ ] }) }),
822
+ /* @__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: [
823
+ /* @__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." }),
824
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
825
+ /* @__PURE__ */ jsxRuntime.jsx(
826
+ ui.Button,
827
+ {
828
+ variant: "secondary",
829
+ size: "small",
830
+ disabled: !isDirty || isSaving,
831
+ onClick: handleReset,
832
+ children: "Reset"
833
+ }
834
+ ),
835
+ /* @__PURE__ */ jsxRuntime.jsx(
836
+ ui.Button,
837
+ {
838
+ size: "small",
839
+ onClick: handleSubmit,
840
+ disabled: !isDirty || hasErrors || isSaving,
841
+ isLoading: isSaving,
842
+ children: "Save metadata"
843
+ }
844
+ )
845
+ ] })
846
+ ] })
847
+ ] })
848
+ ] });
849
+ };
850
+ const ValueField = ({
851
+ descriptor,
852
+ value,
853
+ onStringChange,
854
+ onBooleanChange
855
+ }) => {
856
+ const fileInputRef = react.useRef(null);
857
+ const [isUploading, setIsUploading] = react.useState(false);
858
+ const handleFileUpload = async (event) => {
859
+ var _a;
860
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
861
+ if (!file) {
862
+ return;
863
+ }
864
+ setIsUploading(true);
865
+ try {
866
+ const formData = new FormData();
867
+ formData.append("files", file);
868
+ const response = await fetch("/admin/uploads", {
869
+ method: "POST",
870
+ credentials: "include",
871
+ body: formData
872
+ });
873
+ if (!response.ok) {
874
+ const payload = await response.json().catch(() => null);
875
+ throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
876
+ }
877
+ const result = await response.json();
878
+ if (result.files && result.files.length > 0) {
879
+ const uploadedFile = result.files[0];
880
+ const fileUrl = uploadedFile.url || uploadedFile.key;
881
+ if (fileUrl) {
882
+ onStringChange(descriptor.key, fileUrl);
883
+ ui.toast.success("File uploaded successfully");
884
+ } else {
885
+ throw new Error("File upload succeeded but no URL returned");
886
+ }
887
+ } else {
888
+ throw new Error("File upload failed - no files returned");
889
+ }
890
+ } catch (error) {
891
+ ui.toast.error(
892
+ error instanceof Error ? error.message : "Failed to upload file"
893
+ );
894
+ } finally {
895
+ setIsUploading(false);
896
+ if (fileInputRef.current) {
897
+ fileInputRef.current.value = "";
898
+ }
899
+ }
900
+ };
901
+ if (descriptor.type === "bool") {
902
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
903
+ /* @__PURE__ */ jsxRuntime.jsx(
904
+ ui.Switch,
905
+ {
906
+ checked: Boolean(value),
907
+ onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
908
+ "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
909
+ }
910
+ ),
911
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
912
+ ] });
913
+ }
914
+ if (descriptor.type === "text") {
915
+ return /* @__PURE__ */ jsxRuntime.jsx(
916
+ ui.Textarea,
917
+ {
918
+ value: value ?? "",
919
+ placeholder: "Enter text",
920
+ rows: 3,
921
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
922
+ }
923
+ );
924
+ }
925
+ if (descriptor.type === "number") {
926
+ return /* @__PURE__ */ jsxRuntime.jsx(
927
+ ui.Input,
928
+ {
929
+ type: "text",
930
+ inputMode: "decimal",
931
+ placeholder: "0.00",
932
+ value: value ?? "",
933
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
934
+ }
935
+ );
936
+ }
937
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
938
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
939
+ /* @__PURE__ */ jsxRuntime.jsx(
940
+ ui.Input,
941
+ {
942
+ type: "url",
943
+ placeholder: "https://example.com/file",
944
+ value: value ?? "",
945
+ onChange: (event) => onStringChange(descriptor.key, event.target.value),
946
+ className: "flex-1"
947
+ }
948
+ ),
949
+ /* @__PURE__ */ jsxRuntime.jsx(
950
+ "input",
951
+ {
952
+ ref: fileInputRef,
953
+ type: "file",
954
+ className: "hidden",
955
+ onChange: handleFileUpload,
956
+ disabled: isUploading,
957
+ "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
958
+ }
959
+ ),
960
+ /* @__PURE__ */ jsxRuntime.jsx(
961
+ ui.Button,
962
+ {
963
+ type: "button",
964
+ variant: "secondary",
965
+ size: "small",
966
+ onClick: () => {
967
+ var _a;
968
+ return (_a = fileInputRef.current) == null ? void 0 : _a.click();
969
+ },
970
+ disabled: isUploading,
971
+ isLoading: isUploading,
972
+ children: isUploading ? "Uploading..." : "Upload"
973
+ }
974
+ )
975
+ ] }),
976
+ typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
977
+ "a",
978
+ {
979
+ className: "txt-compact-small-plus text-ui-fg-interactive underline",
980
+ href: value,
981
+ target: "_blank",
982
+ rel: "noreferrer",
983
+ children: "View file"
984
+ }
985
+ )
986
+ ] });
987
+ };
988
+ adminSdk.defineWidgetConfig({
989
+ zone: "product.details.after"
990
+ });
991
+ const fetchJson = async (path) => {
992
+ const response = await fetch(path, {
993
+ credentials: "include"
994
+ });
995
+ const payload = await response.json().catch(() => null);
996
+ if (!response.ok) {
997
+ throw new Error(
998
+ (payload == null ? void 0 : payload.message) ?? "Unable to load wishlist statistics from the server"
999
+ );
1000
+ }
1001
+ return payload;
1002
+ };
1003
+ const ProductWishlistStatsWidget = ({ data }) => {
1004
+ const productId = data == null ? void 0 : data.id;
1005
+ const {
1006
+ data: productStats,
1007
+ isPending: isProductStatsPending,
1008
+ isError: isProductStatsError,
1009
+ error: productStatsError
1010
+ } = reactQuery.useQuery({
1011
+ queryKey: ["wishlist", "product", productId],
1012
+ enabled: Boolean(productId),
1013
+ queryFn: () => fetchJson(
1014
+ `/admin/wishlist/stats?product_id=${productId}`
1015
+ ),
1016
+ refetchInterval: 6e4
1017
+ });
1018
+ const {
1019
+ data: allStats,
1020
+ isPending: isAllStatsPending,
1021
+ isError: isAllStatsError,
1022
+ error: allStatsError
1023
+ } = reactQuery.useQuery({
1024
+ queryKey: ["wishlist", "stats"],
1025
+ queryFn: () => fetchJson("/admin/wishlist/stats"),
1026
+ staleTime: 6e4
1027
+ });
1028
+ const topFive = react.useMemo(() => {
1029
+ var _a;
1030
+ if (!((_a = allStats == null ? void 0 : allStats.stats) == null ? void 0 : _a.length)) {
1031
+ return [];
1032
+ }
1033
+ return [...allStats.stats].sort((a, b) => b.wishlist_count - a.wishlist_count).slice(0, 5);
1034
+ }, [allStats]);
1035
+ const productWishlistCount = (productStats == null ? void 0 : productStats.wishlist_count) ?? 0;
1036
+ const productIsTopWishlisted = topFive.some(
1037
+ (stat) => stat.product_id === productId
1038
+ );
1039
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
1040
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
1041
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Wishlist performance" }),
1042
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Track how often this product appears in customer wishlists and see the current top performers." })
1043
+ ] }),
1044
+ !productId ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: "Product not loaded yet", children: "Open a product detail record to view wishlist insights." }) : isProductStatsPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[96px] w-full rounded-lg" }) : isProductStatsError ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "error", label: "Unable to load product stats", children: productStatsError instanceof Error ? productStatsError.message : "Unknown error" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 rounded-lg border border-ui-border-base bg-ui-bg-base p-4", children: [
1045
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1046
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "This product" }),
1047
+ productIsTopWishlisted && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", color: "green", children: "Top 5" })
1048
+ ] }),
1049
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1050
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", className: "text-[32px] leading-none", children: productWishlistCount }),
1051
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: productWishlistCount === 1 ? "customer has saved this product." : "customers have saved this product." })
1052
+ ] })
1053
+ ] }),
1054
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-3 rounded-lg border border-ui-border-base bg-ui-bg-subtle p-4", children: [
1055
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
1056
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1057
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "Storewide insights" }),
1058
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Top wishlisted products" })
1059
+ ] }),
1060
+ productId && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: productIsTopWishlisted ? "In top 5" : "Not in top 5" })
1061
+ ] }),
1062
+ isAllStatsPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full rounded-lg" }) : isAllStatsError ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "error", label: "Unable to load storewide stats", children: allStatsError instanceof Error ? allStatsError.message : "Unknown error" }) : !topFive.length ? /* @__PURE__ */ jsxRuntime.jsx(ui.InlineTip, { variant: "info", label: "No wishlist activity yet", children: "Customers have not saved any products to their wishlists yet. Once they do, the most popular products will show up here." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-base", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1063
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-field", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1064
+ /* @__PURE__ */ jsxRuntime.jsx(
1065
+ "th",
1066
+ {
1067
+ scope: "col",
1068
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1069
+ children: "#"
1070
+ }
1071
+ ),
1072
+ /* @__PURE__ */ jsxRuntime.jsx(
1073
+ "th",
1074
+ {
1075
+ scope: "col",
1076
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1077
+ children: "Product ID"
1078
+ }
1079
+ ),
1080
+ /* @__PURE__ */ jsxRuntime.jsx(
1081
+ "th",
1082
+ {
1083
+ scope: "col",
1084
+ className: "px-4 py-2 text-right text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1085
+ children: "Wishlists"
1086
+ }
1087
+ )
1088
+ ] }) }),
1089
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: topFive.map((stat, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1090
+ "tr",
1091
+ {
1092
+ className: stat.product_id === productId ? "bg-ui-bg-subtle" : "bg-ui-bg-base",
1093
+ children: [
1094
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "px-4 py-3 text-sm font-medium text-ui-fg-subtle", children: [
1095
+ "#",
1096
+ index + 1
1097
+ ] }),
1098
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
1099
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-mono text-sm", children: stat.product_id }),
1100
+ stat.product_id === productId && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "green", children: "Current" })
1101
+ ] }) }),
1102
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-4 py-3 text-right text-sm font-semibold", children: stat.wishlist_count.toLocaleString() })
1103
+ ]
1104
+ },
1105
+ stat.product_id
1106
+ )) })
1107
+ ] }) })
1108
+ ] })
1109
+ ] });
1110
+ };
1111
+ adminSdk.defineWidgetConfig({
1112
+ zone: "product.details.side.after"
1113
+ });
1114
+ const i18nTranslations0 = {};
1115
+ const widgetModule = { widgets: [
1116
+ {
1117
+ Component: CategoryMetadataTableWidget,
1118
+ zone: ["product_category.details.after"]
1119
+ },
1120
+ {
1121
+ Component: HideCategoryDefaultMetadataWidget,
1122
+ zone: ["product_category.details.side.before"]
1123
+ },
1124
+ {
1125
+ Component: HideDefaultMetadataWidget,
1126
+ zone: ["product.details.side.before"]
1127
+ },
1128
+ {
1129
+ Component: ProductMetadataTableWidget,
1130
+ zone: ["product.details.after"]
1131
+ },
1132
+ {
1133
+ Component: ProductWishlistStatsWidget,
1134
+ zone: ["product.details.side.after"]
602
1135
  }
603
1136
  ] };
604
1137
  const routeModule = {