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,61 +1,8 @@
1
- import { defineWidgetConfig } from "@medusajs/admin-sdk";
2
- import { useEffect, useState, useMemo, useRef } from "react";
3
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { defineWidgetConfig } from "@medusajs/admin-sdk";
4
3
  import { Container, Heading, Badge, Text, Skeleton, InlineTip, Button, Switch, Textarea, Input, toast } from "@medusajs/ui";
4
+ import { useState, useEffect, useMemo, useRef } from "react";
5
5
  import { useQuery, useQueryClient } from "@tanstack/react-query";
6
- const HideDefaultMetadataWidget = () => {
7
- useEffect(() => {
8
- const hideMetadataSection = () => {
9
- const headings = document.querySelectorAll("h2");
10
- headings.forEach((heading) => {
11
- var _a;
12
- if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
13
- let container = heading.parentElement;
14
- while (container && container !== document.body) {
15
- const hasContainerClass = container.classList.toString().includes("Container");
16
- const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
17
- if (hasContainerClass || isInSidebar) {
18
- const editLink = container.querySelector('a[href*="metadata/edit"]');
19
- const badge = container.querySelector('div[class*="Badge"]');
20
- if (editLink && badge) {
21
- container.style.display = "none";
22
- container.setAttribute("data-metadata-hidden", "true");
23
- return;
24
- }
25
- }
26
- container = container.parentElement;
27
- }
28
- }
29
- });
30
- };
31
- const runHide = () => {
32
- setTimeout(hideMetadataSection, 100);
33
- };
34
- runHide();
35
- const observer = new MutationObserver(() => {
36
- const alreadyHidden = document.querySelector('[data-metadata-hidden="true"]');
37
- if (!alreadyHidden) {
38
- runHide();
39
- }
40
- });
41
- observer.observe(document.body, {
42
- childList: true,
43
- subtree: true
44
- });
45
- return () => {
46
- observer.disconnect();
47
- const hidden = document.querySelector('[data-metadata-hidden="true"]');
48
- if (hidden) {
49
- hidden.style.display = "";
50
- hidden.removeAttribute("data-metadata-hidden");
51
- }
52
- };
53
- }, []);
54
- return null;
55
- };
56
- defineWidgetConfig({
57
- zone: "product.details.side.before"
58
- });
59
6
  const METADATA_FIELD_TYPES = ["number", "text", "file", "bool"];
60
7
  const VALID_FIELD_TYPES = new Set(METADATA_FIELD_TYPES);
61
8
  function normalizeMetadataDescriptors(input) {
@@ -77,10 +24,12 @@ function normalizeMetadataDescriptors(input) {
77
24
  continue;
78
25
  }
79
26
  const label = getNormalizedLabel(item.label);
27
+ const filterable = typeof item.filterable === "boolean" ? item.filterable : Boolean(item.filterable);
80
28
  normalized.push({
81
29
  key,
82
30
  type,
83
- ...label ? { label } : {}
31
+ ...label ? { label } : {},
32
+ ...filterable ? { filterable: true } : {}
84
33
  });
85
34
  seenKeys.add(key);
86
35
  }
@@ -236,11 +185,11 @@ function isDeepEqual(a, b) {
236
185
  }
237
186
  const CONFIG_ENDPOINT = "/admin/product-metadata-config";
238
187
  const QUERY_KEY = ["medusa-product-helper", "metadata-config"];
239
- const useProductMetadataConfig = () => {
188
+ const useMetadataConfig = (entity) => {
240
189
  return useQuery({
241
- queryKey: QUERY_KEY,
190
+ queryKey: [...QUERY_KEY, entity],
242
191
  queryFn: async () => {
243
- const response = await fetch(CONFIG_ENDPOINT, {
192
+ const response = await fetch(`${CONFIG_ENDPOINT}?entity=${entity}`, {
244
193
  credentials: "include"
245
194
  });
246
195
  if (!response.ok) {
@@ -252,9 +201,11 @@ const useProductMetadataConfig = () => {
252
201
  staleTime: 5 * 60 * 1e3
253
202
  });
254
203
  };
255
- const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
256
- const ProductMetadataTableWidget = ({ data }) => {
257
- const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
204
+ const useProductMetadataConfig = () => useMetadataConfig("product");
205
+ const useCategoryMetadataConfig = () => useMetadataConfig("category");
206
+ const CONFIG_DOCS_URL$1 = "https://docs.medusajs.com/admin/extension-points/widgets#product-category-details";
207
+ const CategoryMetadataTableWidget = ({ data }) => {
208
+ const { data: descriptors = [], isPending, isError } = useCategoryMetadataConfig();
258
209
  const metadata = (data == null ? void 0 : data.metadata) ?? {};
259
210
  const [baselineMetadata, setBaselineMetadata] = useState(metadata);
260
211
  const queryClient = useQueryClient();
@@ -315,7 +266,7 @@ const ProductMetadataTableWidget = ({ data }) => {
315
266
  values,
316
267
  originalMetadata: baselineMetadata
317
268
  });
318
- const response = await fetch(`/admin/products/${data.id}`, {
269
+ const response = await fetch(`/admin/product-categories/${data.id}`, {
319
270
  method: "POST",
320
271
  credentials: "include",
321
272
  headers: {
@@ -330,12 +281,12 @@ const ProductMetadataTableWidget = ({ data }) => {
330
281
  throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
331
282
  }
332
283
  const updated = await response.json();
333
- const nextMetadata = updated.product.metadata;
284
+ const nextMetadata = updated.product_category.metadata;
334
285
  setBaselineMetadata(nextMetadata);
335
286
  setValues(buildInitialFormState(descriptors, nextMetadata));
336
287
  toast.success("Metadata saved");
337
288
  await queryClient.invalidateQueries({
338
- queryKey: ["products"]
289
+ queryKey: ["product-categories"]
339
290
  });
340
291
  } catch (error) {
341
292
  toast.error(error instanceof Error ? error.message : "Save failed");
@@ -364,7 +315,7 @@ const ProductMetadataTableWidget = ({ data }) => {
364
315
  "a",
365
316
  {
366
317
  className: "text-ui-fg-interactive underline",
367
- href: CONFIG_DOCS_URL,
318
+ href: CONFIG_DOCS_URL$1,
368
319
  target: "_blank",
369
320
  rel: "noreferrer",
370
321
  children: "Learn how to configure it."
@@ -407,7 +358,7 @@ const ProductMetadataTableWidget = ({ data }) => {
407
358
  ),
408
359
  /* @__PURE__ */ jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
409
360
  /* @__PURE__ */ jsx(
410
- ValueField,
361
+ ValueField$1,
411
362
  {
412
363
  descriptor,
413
364
  value,
@@ -421,7 +372,7 @@ const ProductMetadataTableWidget = ({ data }) => {
421
372
  }) })
422
373
  ] }) }),
423
374
  /* @__PURE__ */ 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: [
424
- /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Changes are stored on the product metadata object. Clearing a field removes the corresponding key on save." }),
375
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Changes are stored on the category metadata object. Clearing a field removes the corresponding key on save." }),
425
376
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
426
377
  /* @__PURE__ */ jsx(
427
378
  Button,
@@ -448,7 +399,7 @@ const ProductMetadataTableWidget = ({ data }) => {
448
399
  ] })
449
400
  ] });
450
401
  };
451
- const ValueField = ({
402
+ const ValueField$1 = ({
452
403
  descriptor,
453
404
  value,
454
405
  onStringChange,
@@ -587,17 +538,599 @@ const ValueField = ({
587
538
  ] });
588
539
  };
589
540
  defineWidgetConfig({
590
- zone: "product.details.after"
541
+ zone: "product_category.details.after"
591
542
  });
592
- const i18nTranslations0 = {};
593
- const widgetModule = { widgets: [
594
- {
595
- Component: HideDefaultMetadataWidget,
596
- zone: ["product.details.side.before"]
597
- },
598
- {
599
- Component: ProductMetadataTableWidget,
600
- zone: ["product.details.after"]
543
+ const HideCategoryDefaultMetadataWidget = () => {
544
+ useEffect(() => {
545
+ const hideMetadataSection = () => {
546
+ const headings = document.querySelectorAll("h2");
547
+ headings.forEach((heading) => {
548
+ var _a;
549
+ if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
550
+ let container = heading.parentElement;
551
+ while (container && container !== document.body) {
552
+ const hasContainerClass = container.classList.toString().includes("Container");
553
+ const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
554
+ if (hasContainerClass || isInSidebar) {
555
+ const editLink = container.querySelector('a[href*="metadata/edit"]');
556
+ const badge = container.querySelector('div[class*="Badge"]');
557
+ if (editLink && badge) {
558
+ container.style.display = "none";
559
+ container.setAttribute("data-category-metadata-hidden", "true");
560
+ return;
561
+ }
562
+ }
563
+ container = container.parentElement;
564
+ }
565
+ }
566
+ });
567
+ };
568
+ const runHide = () => {
569
+ setTimeout(hideMetadataSection, 100);
570
+ };
571
+ runHide();
572
+ const observer = new MutationObserver(() => {
573
+ const alreadyHidden = document.querySelector(
574
+ '[data-category-metadata-hidden="true"]'
575
+ );
576
+ if (!alreadyHidden) {
577
+ runHide();
578
+ }
579
+ });
580
+ observer.observe(document.body, {
581
+ childList: true,
582
+ subtree: true
583
+ });
584
+ return () => {
585
+ observer.disconnect();
586
+ const hidden = document.querySelector(
587
+ '[data-category-metadata-hidden="true"]'
588
+ );
589
+ if (hidden) {
590
+ hidden.style.display = "";
591
+ hidden.removeAttribute("data-category-metadata-hidden");
592
+ }
593
+ };
594
+ }, []);
595
+ return null;
596
+ };
597
+ defineWidgetConfig({
598
+ zone: "product_category.details.side.before"
599
+ });
600
+ const HideDefaultMetadataWidget = () => {
601
+ useEffect(() => {
602
+ const hideMetadataSection = () => {
603
+ const headings = document.querySelectorAll("h2");
604
+ headings.forEach((heading) => {
605
+ var _a;
606
+ if (((_a = heading.textContent) == null ? void 0 : _a.trim()) === "Metadata") {
607
+ let container = heading.parentElement;
608
+ while (container && container !== document.body) {
609
+ const hasContainerClass = container.classList.toString().includes("Container");
610
+ const isInSidebar = container.closest('[class*="Sidebar"]') || container.closest('[class*="sidebar"]');
611
+ if (hasContainerClass || isInSidebar) {
612
+ const editLink = container.querySelector('a[href*="metadata/edit"]');
613
+ const badge = container.querySelector('div[class*="Badge"]');
614
+ if (editLink && badge) {
615
+ container.style.display = "none";
616
+ container.setAttribute("data-metadata-hidden", "true");
617
+ return;
618
+ }
619
+ }
620
+ container = container.parentElement;
621
+ }
622
+ }
623
+ });
624
+ };
625
+ const runHide = () => {
626
+ setTimeout(hideMetadataSection, 100);
627
+ };
628
+ runHide();
629
+ const observer = new MutationObserver(() => {
630
+ const alreadyHidden = document.querySelector('[data-metadata-hidden="true"]');
631
+ if (!alreadyHidden) {
632
+ runHide();
633
+ }
634
+ });
635
+ observer.observe(document.body, {
636
+ childList: true,
637
+ subtree: true
638
+ });
639
+ return () => {
640
+ observer.disconnect();
641
+ const hidden = document.querySelector('[data-metadata-hidden="true"]');
642
+ if (hidden) {
643
+ hidden.style.display = "";
644
+ hidden.removeAttribute("data-metadata-hidden");
645
+ }
646
+ };
647
+ }, []);
648
+ return null;
649
+ };
650
+ defineWidgetConfig({
651
+ zone: "product.details.side.before"
652
+ });
653
+ const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
654
+ const ProductMetadataTableWidget = ({ data }) => {
655
+ const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
656
+ const metadata = (data == null ? void 0 : data.metadata) ?? {};
657
+ const [baselineMetadata, setBaselineMetadata] = useState(metadata);
658
+ const queryClient = useQueryClient();
659
+ useEffect(() => {
660
+ setBaselineMetadata(metadata);
661
+ }, [metadata]);
662
+ const initialState = useMemo(
663
+ () => buildInitialFormState(descriptors, baselineMetadata),
664
+ [descriptors, baselineMetadata]
665
+ );
666
+ const [values, setValues] = useState(
667
+ initialState
668
+ );
669
+ const [isSaving, setIsSaving] = useState(false);
670
+ useEffect(() => {
671
+ setValues(initialState);
672
+ }, [initialState]);
673
+ const errors = useMemo(() => {
674
+ return descriptors.reduce((acc, descriptor) => {
675
+ const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
676
+ if (error) {
677
+ acc[descriptor.key] = error;
678
+ }
679
+ return acc;
680
+ }, {});
681
+ }, [descriptors, values]);
682
+ const hasErrors = Object.keys(errors).length > 0;
683
+ const isDirty = useMemo(() => {
684
+ return hasMetadataChanges({
685
+ descriptors,
686
+ values,
687
+ originalMetadata: baselineMetadata
688
+ });
689
+ }, [descriptors, values, baselineMetadata]);
690
+ const handleStringChange = (key, nextValue) => {
691
+ setValues((prev) => ({
692
+ ...prev,
693
+ [key]: nextValue
694
+ }));
695
+ };
696
+ const handleBooleanChange = (key, nextValue) => {
697
+ setValues((prev) => ({
698
+ ...prev,
699
+ [key]: nextValue
700
+ }));
701
+ };
702
+ const handleReset = () => {
703
+ setValues(initialState);
704
+ };
705
+ const handleSubmit = async () => {
706
+ if (!(data == null ? void 0 : data.id) || !descriptors.length) {
707
+ return;
708
+ }
709
+ setIsSaving(true);
710
+ try {
711
+ const metadataPayload = buildMetadataPayload({
712
+ descriptors,
713
+ values,
714
+ originalMetadata: baselineMetadata
715
+ });
716
+ const response = await fetch(`/admin/products/${data.id}`, {
717
+ method: "POST",
718
+ credentials: "include",
719
+ headers: {
720
+ "Content-Type": "application/json"
721
+ },
722
+ body: JSON.stringify({
723
+ metadata: metadataPayload
724
+ })
725
+ });
726
+ if (!response.ok) {
727
+ const payload = await response.json().catch(() => null);
728
+ throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
729
+ }
730
+ const updated = await response.json();
731
+ const nextMetadata = updated.product.metadata;
732
+ setBaselineMetadata(nextMetadata);
733
+ setValues(buildInitialFormState(descriptors, nextMetadata));
734
+ toast.success("Metadata saved");
735
+ await queryClient.invalidateQueries({
736
+ queryKey: ["products"]
737
+ });
738
+ } catch (error) {
739
+ toast.error(error instanceof Error ? error.message : "Save failed");
740
+ } finally {
741
+ setIsSaving(false);
742
+ }
743
+ };
744
+ return /* @__PURE__ */ jsxs(Container, { className: "flex flex-col gap-y-4", children: [
745
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-y-1", children: [
746
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3", children: [
747
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Metadata" }),
748
+ /* @__PURE__ */ jsx(Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
749
+ ] }),
750
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
751
+ ] }),
752
+ isPending ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxs(InlineTip, { variant: "error", label: "Configuration unavailable", children: [
753
+ "Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
754
+ /* @__PURE__ */ jsx("code", { children: "medusa-config.ts" }),
755
+ "."
756
+ ] }) : !descriptors.length ? /* @__PURE__ */ jsxs(InlineTip, { variant: "info", label: "No configured metadata keys", children: [
757
+ "Provide a ",
758
+ /* @__PURE__ */ jsx("code", { children: "metadataDescriptors" }),
759
+ " array in the plugin options to control which keys show up here.",
760
+ " ",
761
+ /* @__PURE__ */ jsx(
762
+ "a",
763
+ {
764
+ className: "text-ui-fg-interactive underline",
765
+ href: CONFIG_DOCS_URL,
766
+ target: "_blank",
767
+ rel: "noreferrer",
768
+ children: "Learn how to configure it."
769
+ }
770
+ )
771
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
772
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
773
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxs("tr", { children: [
774
+ /* @__PURE__ */ jsx(
775
+ "th",
776
+ {
777
+ scope: "col",
778
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
779
+ children: "Label"
780
+ }
781
+ ),
782
+ /* @__PURE__ */ jsx(
783
+ "th",
784
+ {
785
+ scope: "col",
786
+ className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
787
+ children: "Value"
788
+ }
789
+ )
790
+ ] }) }),
791
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
792
+ const value = values[descriptor.key];
793
+ const error = errors[descriptor.key];
794
+ return /* @__PURE__ */ jsxs("tr", { children: [
795
+ /* @__PURE__ */ jsx(
796
+ "th",
797
+ {
798
+ scope: "row",
799
+ className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
800
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
801
+ /* @__PURE__ */ jsx("span", { children: descriptor.label ?? descriptor.key }),
802
+ /* @__PURE__ */ jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
803
+ ] })
804
+ }
805
+ ),
806
+ /* @__PURE__ */ jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
807
+ /* @__PURE__ */ jsx(
808
+ ValueField,
809
+ {
810
+ descriptor,
811
+ value,
812
+ onStringChange: handleStringChange,
813
+ onBooleanChange: handleBooleanChange
814
+ }
815
+ ),
816
+ error && /* @__PURE__ */ jsx(Text, { className: "txt-compact-small text-ui-fg-error", children: error })
817
+ ] }) })
818
+ ] }, descriptor.key);
819
+ }) })
820
+ ] }) }),
821
+ /* @__PURE__ */ 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: [
822
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Changes are stored on the product metadata object. Clearing a field removes the corresponding key on save." }),
823
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
824
+ /* @__PURE__ */ jsx(
825
+ Button,
826
+ {
827
+ variant: "secondary",
828
+ size: "small",
829
+ disabled: !isDirty || isSaving,
830
+ onClick: handleReset,
831
+ children: "Reset"
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsx(
835
+ Button,
836
+ {
837
+ size: "small",
838
+ onClick: handleSubmit,
839
+ disabled: !isDirty || hasErrors || isSaving,
840
+ isLoading: isSaving,
841
+ children: "Save metadata"
842
+ }
843
+ )
844
+ ] })
845
+ ] })
846
+ ] })
847
+ ] });
848
+ };
849
+ const ValueField = ({
850
+ descriptor,
851
+ value,
852
+ onStringChange,
853
+ onBooleanChange
854
+ }) => {
855
+ const fileInputRef = useRef(null);
856
+ const [isUploading, setIsUploading] = useState(false);
857
+ const handleFileUpload = async (event) => {
858
+ var _a;
859
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
860
+ if (!file) {
861
+ return;
862
+ }
863
+ setIsUploading(true);
864
+ try {
865
+ const formData = new FormData();
866
+ formData.append("files", file);
867
+ const response = await fetch("/admin/uploads", {
868
+ method: "POST",
869
+ credentials: "include",
870
+ body: formData
871
+ });
872
+ if (!response.ok) {
873
+ const payload = await response.json().catch(() => null);
874
+ throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
875
+ }
876
+ const result = await response.json();
877
+ if (result.files && result.files.length > 0) {
878
+ const uploadedFile = result.files[0];
879
+ const fileUrl = uploadedFile.url || uploadedFile.key;
880
+ if (fileUrl) {
881
+ onStringChange(descriptor.key, fileUrl);
882
+ toast.success("File uploaded successfully");
883
+ } else {
884
+ throw new Error("File upload succeeded but no URL returned");
885
+ }
886
+ } else {
887
+ throw new Error("File upload failed - no files returned");
888
+ }
889
+ } catch (error) {
890
+ toast.error(
891
+ error instanceof Error ? error.message : "Failed to upload file"
892
+ );
893
+ } finally {
894
+ setIsUploading(false);
895
+ if (fileInputRef.current) {
896
+ fileInputRef.current.value = "";
897
+ }
898
+ }
899
+ };
900
+ if (descriptor.type === "bool") {
901
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
902
+ /* @__PURE__ */ jsx(
903
+ Switch,
904
+ {
905
+ checked: Boolean(value),
906
+ onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
907
+ "aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
908
+ }
909
+ ),
910
+ /* @__PURE__ */ jsx(Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
911
+ ] });
912
+ }
913
+ if (descriptor.type === "text") {
914
+ return /* @__PURE__ */ jsx(
915
+ Textarea,
916
+ {
917
+ value: value ?? "",
918
+ placeholder: "Enter text",
919
+ rows: 3,
920
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
921
+ }
922
+ );
923
+ }
924
+ if (descriptor.type === "number") {
925
+ return /* @__PURE__ */ jsx(
926
+ Input,
927
+ {
928
+ type: "text",
929
+ inputMode: "decimal",
930
+ placeholder: "0.00",
931
+ value: value ?? "",
932
+ onChange: (event) => onStringChange(descriptor.key, event.target.value)
933
+ }
934
+ );
935
+ }
936
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
937
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
938
+ /* @__PURE__ */ jsx(
939
+ Input,
940
+ {
941
+ type: "url",
942
+ placeholder: "https://example.com/file",
943
+ value: value ?? "",
944
+ onChange: (event) => onStringChange(descriptor.key, event.target.value),
945
+ className: "flex-1"
946
+ }
947
+ ),
948
+ /* @__PURE__ */ jsx(
949
+ "input",
950
+ {
951
+ ref: fileInputRef,
952
+ type: "file",
953
+ className: "hidden",
954
+ onChange: handleFileUpload,
955
+ disabled: isUploading,
956
+ "aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
957
+ }
958
+ ),
959
+ /* @__PURE__ */ jsx(
960
+ Button,
961
+ {
962
+ type: "button",
963
+ variant: "secondary",
964
+ size: "small",
965
+ onClick: () => {
966
+ var _a;
967
+ return (_a = fileInputRef.current) == null ? void 0 : _a.click();
968
+ },
969
+ disabled: isUploading,
970
+ isLoading: isUploading,
971
+ children: isUploading ? "Uploading..." : "Upload"
972
+ }
973
+ )
974
+ ] }),
975
+ typeof value === "string" && value && /* @__PURE__ */ jsx(
976
+ "a",
977
+ {
978
+ className: "txt-compact-small-plus text-ui-fg-interactive underline",
979
+ href: value,
980
+ target: "_blank",
981
+ rel: "noreferrer",
982
+ children: "View file"
983
+ }
984
+ )
985
+ ] });
986
+ };
987
+ defineWidgetConfig({
988
+ zone: "product.details.after"
989
+ });
990
+ const fetchJson = async (path) => {
991
+ const response = await fetch(path, {
992
+ credentials: "include"
993
+ });
994
+ const payload = await response.json().catch(() => null);
995
+ if (!response.ok) {
996
+ throw new Error(
997
+ (payload == null ? void 0 : payload.message) ?? "Unable to load wishlist statistics from the server"
998
+ );
999
+ }
1000
+ return payload;
1001
+ };
1002
+ const ProductWishlistStatsWidget = ({ data }) => {
1003
+ const productId = data == null ? void 0 : data.id;
1004
+ const {
1005
+ data: productStats,
1006
+ isPending: isProductStatsPending,
1007
+ isError: isProductStatsError,
1008
+ error: productStatsError
1009
+ } = useQuery({
1010
+ queryKey: ["wishlist", "product", productId],
1011
+ enabled: Boolean(productId),
1012
+ queryFn: () => fetchJson(
1013
+ `/admin/wishlist/stats?product_id=${productId}`
1014
+ ),
1015
+ refetchInterval: 6e4
1016
+ });
1017
+ const {
1018
+ data: allStats,
1019
+ isPending: isAllStatsPending,
1020
+ isError: isAllStatsError,
1021
+ error: allStatsError
1022
+ } = useQuery({
1023
+ queryKey: ["wishlist", "stats"],
1024
+ queryFn: () => fetchJson("/admin/wishlist/stats"),
1025
+ staleTime: 6e4
1026
+ });
1027
+ const topFive = useMemo(() => {
1028
+ var _a;
1029
+ if (!((_a = allStats == null ? void 0 : allStats.stats) == null ? void 0 : _a.length)) {
1030
+ return [];
1031
+ }
1032
+ return [...allStats.stats].sort((a, b) => b.wishlist_count - a.wishlist_count).slice(0, 5);
1033
+ }, [allStats]);
1034
+ const productWishlistCount = (productStats == null ? void 0 : productStats.wishlist_count) ?? 0;
1035
+ const productIsTopWishlisted = topFive.some(
1036
+ (stat) => stat.product_id === productId
1037
+ );
1038
+ return /* @__PURE__ */ jsxs(Container, { className: "flex flex-col gap-y-4", children: [
1039
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-y-1", children: [
1040
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Wishlist performance" }),
1041
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: "Track how often this product appears in customer wishlists and see the current top performers." })
1042
+ ] }),
1043
+ !productId ? /* @__PURE__ */ jsx(InlineTip, { variant: "info", label: "Product not loaded yet", children: "Open a product detail record to view wishlist insights." }) : isProductStatsPending ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[96px] w-full rounded-lg" }) : isProductStatsError ? /* @__PURE__ */ jsx(InlineTip, { variant: "error", label: "Unable to load product stats", children: productStatsError instanceof Error ? productStatsError.message : "Unknown error" }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 rounded-lg border border-ui-border-base bg-ui-bg-base p-4", children: [
1044
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
1045
+ /* @__PURE__ */ jsx(Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "This product" }),
1046
+ productIsTopWishlisted && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", rounded: "full", color: "green", children: "Top 5" })
1047
+ ] }),
1048
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
1049
+ /* @__PURE__ */ jsx(Heading, { level: "h1", className: "text-[32px] leading-none", children: productWishlistCount }),
1050
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", children: productWishlistCount === 1 ? "customer has saved this product." : "customers have saved this product." })
1051
+ ] })
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3 rounded-lg border border-ui-border-base bg-ui-bg-subtle p-4", children: [
1054
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-x-4", children: [
1055
+ /* @__PURE__ */ jsxs("div", { children: [
1056
+ /* @__PURE__ */ jsx(Text, { className: "txt-compact-xsmall-plus uppercase tracking-wide text-ui-fg-muted", children: "Storewide insights" }),
1057
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Top wishlisted products" })
1058
+ ] }),
1059
+ productId && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: productIsTopWishlisted ? "In top 5" : "Not in top 5" })
1060
+ ] }),
1061
+ isAllStatsPending ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[160px] w-full rounded-lg" }) : isAllStatsError ? /* @__PURE__ */ jsx(InlineTip, { variant: "error", label: "Unable to load storewide stats", children: allStatsError instanceof Error ? allStatsError.message : "Unknown error" }) : !topFive.length ? /* @__PURE__ */ jsx(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__ */ jsx("div", { className: "overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-base", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-ui-border-base", children: [
1062
+ /* @__PURE__ */ jsx("thead", { className: "bg-ui-bg-field", children: /* @__PURE__ */ jsxs("tr", { children: [
1063
+ /* @__PURE__ */ jsx(
1064
+ "th",
1065
+ {
1066
+ scope: "col",
1067
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1068
+ children: "#"
1069
+ }
1070
+ ),
1071
+ /* @__PURE__ */ jsx(
1072
+ "th",
1073
+ {
1074
+ scope: "col",
1075
+ className: "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1076
+ children: "Product ID"
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsx(
1080
+ "th",
1081
+ {
1082
+ scope: "col",
1083
+ className: "px-4 py-2 text-right text-[11px] font-semibold uppercase tracking-wide text-ui-fg-muted",
1084
+ children: "Wishlists"
1085
+ }
1086
+ )
1087
+ ] }) }),
1088
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-ui-border-subtle", children: topFive.map((stat, index) => /* @__PURE__ */ jsxs(
1089
+ "tr",
1090
+ {
1091
+ className: stat.product_id === productId ? "bg-ui-bg-subtle" : "bg-ui-bg-base",
1092
+ children: [
1093
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3 text-sm font-medium text-ui-fg-subtle", children: [
1094
+ "#",
1095
+ index + 1
1096
+ ] }),
1097
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
1098
+ /* @__PURE__ */ jsx(Text, { className: "font-mono text-sm", children: stat.product_id }),
1099
+ stat.product_id === productId && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "green", children: "Current" })
1100
+ ] }) }),
1101
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-right text-sm font-semibold", children: stat.wishlist_count.toLocaleString() })
1102
+ ]
1103
+ },
1104
+ stat.product_id
1105
+ )) })
1106
+ ] }) })
1107
+ ] })
1108
+ ] });
1109
+ };
1110
+ defineWidgetConfig({
1111
+ zone: "product.details.side.after"
1112
+ });
1113
+ const i18nTranslations0 = {};
1114
+ const widgetModule = { widgets: [
1115
+ {
1116
+ Component: CategoryMetadataTableWidget,
1117
+ zone: ["product_category.details.after"]
1118
+ },
1119
+ {
1120
+ Component: HideCategoryDefaultMetadataWidget,
1121
+ zone: ["product_category.details.side.before"]
1122
+ },
1123
+ {
1124
+ Component: HideDefaultMetadataWidget,
1125
+ zone: ["product.details.side.before"]
1126
+ },
1127
+ {
1128
+ Component: ProductMetadataTableWidget,
1129
+ zone: ["product.details.after"]
1130
+ },
1131
+ {
1132
+ Component: ProductWishlistStatsWidget,
1133
+ zone: ["product.details.side.after"]
601
1134
  }
602
1135
  ] };
603
1136
  const routeModule = {