medusa-product-helper 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +346 -4
- package/.medusa/server/src/admin/index.mjs +346 -4
- package/.medusa/server/src/api/admin/product-metadata-config/route.js +5 -2
- package/.medusa/server/src/api/store/product-helper/products/route.js +105 -11
- package/.medusa/server/src/api/store/product-helper/products/validators.js +10 -1
- package/.medusa/server/src/config/product-helper-options.js +11 -1
- package/.medusa/server/src/providers/filter-providers/metadata-provider.js +9 -5
- package/.medusa/server/src/services/dynamic-filter-service.js +471 -96
- package/.medusa/server/src/services/product-filter-service.js +61 -3
- package/package.json +1 -1
|
@@ -146,7 +146,8 @@ const useMetadataConfig = (entity) => {
|
|
|
146
146
|
};
|
|
147
147
|
const useProductMetadataConfig = () => useMetadataConfig("product");
|
|
148
148
|
const useCategoryMetadataConfig = () => useMetadataConfig("category");
|
|
149
|
-
const
|
|
149
|
+
const useOrderMetadataConfig = () => useMetadataConfig("order");
|
|
150
|
+
const CONFIG_DOCS_URL$2 = "https://docs.medusajs.com/admin/extension-points/widgets#product-category-details";
|
|
150
151
|
const CategoryMetadataTableWidget = ({ data }) => {
|
|
151
152
|
const { data: descriptors = [], isPending, isError } = useCategoryMetadataConfig();
|
|
152
153
|
const metadata = (data == null ? void 0 : data.metadata) ?? {};
|
|
@@ -258,7 +259,7 @@ const CategoryMetadataTableWidget = ({ data }) => {
|
|
|
258
259
|
"a",
|
|
259
260
|
{
|
|
260
261
|
className: "text-ui-fg-interactive underline",
|
|
261
|
-
href: CONFIG_DOCS_URL$
|
|
262
|
+
href: CONFIG_DOCS_URL$2,
|
|
262
263
|
target: "_blank",
|
|
263
264
|
rel: "noreferrer",
|
|
264
265
|
children: "Learn how to configure it."
|
|
@@ -301,7 +302,7 @@ const CategoryMetadataTableWidget = ({ data }) => {
|
|
|
301
302
|
),
|
|
302
303
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
303
304
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
304
|
-
ValueField$
|
|
305
|
+
ValueField$2,
|
|
305
306
|
{
|
|
306
307
|
descriptor,
|
|
307
308
|
value,
|
|
@@ -342,7 +343,7 @@ const CategoryMetadataTableWidget = ({ data }) => {
|
|
|
342
343
|
] })
|
|
343
344
|
] });
|
|
344
345
|
};
|
|
345
|
-
const ValueField$
|
|
346
|
+
const ValueField$2 = ({
|
|
346
347
|
descriptor,
|
|
347
348
|
value,
|
|
348
349
|
onStringChange,
|
|
@@ -593,6 +594,343 @@ const HideDefaultMetadataWidget = () => {
|
|
|
593
594
|
adminSdk.defineWidgetConfig({
|
|
594
595
|
zone: "product.details.side.before"
|
|
595
596
|
});
|
|
597
|
+
const CONFIG_DOCS_URL$1 = "https://docs.medusajs.com/admin/extension-points/widgets#order-details";
|
|
598
|
+
const OrderMetadataTableWidget = ({ data }) => {
|
|
599
|
+
const { data: descriptors = [], isPending, isError } = useOrderMetadataConfig();
|
|
600
|
+
const metadata = (data == null ? void 0 : data.metadata) ?? {};
|
|
601
|
+
const [baselineMetadata, setBaselineMetadata] = react.useState(metadata);
|
|
602
|
+
const queryClient = reactQuery.useQueryClient();
|
|
603
|
+
react.useEffect(() => {
|
|
604
|
+
setBaselineMetadata(metadata);
|
|
605
|
+
}, [metadata]);
|
|
606
|
+
const initialState = react.useMemo(
|
|
607
|
+
() => buildInitialFormState(descriptors, baselineMetadata),
|
|
608
|
+
[descriptors, baselineMetadata]
|
|
609
|
+
);
|
|
610
|
+
const [values, setValues] = react.useState(
|
|
611
|
+
initialState
|
|
612
|
+
);
|
|
613
|
+
const [isSaving, setIsSaving] = react.useState(false);
|
|
614
|
+
react.useEffect(() => {
|
|
615
|
+
setValues(initialState);
|
|
616
|
+
}, [initialState]);
|
|
617
|
+
const errors = react.useMemo(() => {
|
|
618
|
+
return descriptors.reduce((acc, descriptor) => {
|
|
619
|
+
const error = validateValueForDescriptor(descriptor, values[descriptor.key]);
|
|
620
|
+
if (error) {
|
|
621
|
+
acc[descriptor.key] = error;
|
|
622
|
+
}
|
|
623
|
+
return acc;
|
|
624
|
+
}, {});
|
|
625
|
+
}, [descriptors, values]);
|
|
626
|
+
const hasErrors = Object.keys(errors).length > 0;
|
|
627
|
+
const isDirty = react.useMemo(() => {
|
|
628
|
+
return hasMetadataChanges({
|
|
629
|
+
descriptors,
|
|
630
|
+
values,
|
|
631
|
+
originalMetadata: baselineMetadata
|
|
632
|
+
});
|
|
633
|
+
}, [descriptors, values, baselineMetadata]);
|
|
634
|
+
const handleStringChange = (key, nextValue) => {
|
|
635
|
+
setValues((prev) => ({
|
|
636
|
+
...prev,
|
|
637
|
+
[key]: nextValue
|
|
638
|
+
}));
|
|
639
|
+
};
|
|
640
|
+
const handleBooleanChange = (key, nextValue) => {
|
|
641
|
+
setValues((prev) => ({
|
|
642
|
+
...prev,
|
|
643
|
+
[key]: nextValue
|
|
644
|
+
}));
|
|
645
|
+
};
|
|
646
|
+
const handleReset = () => {
|
|
647
|
+
setValues(initialState);
|
|
648
|
+
};
|
|
649
|
+
const handleSubmit = async () => {
|
|
650
|
+
if (!(data == null ? void 0 : data.id) || !descriptors.length) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
setIsSaving(true);
|
|
654
|
+
try {
|
|
655
|
+
const metadataPayload = buildMetadataPayload({
|
|
656
|
+
descriptors,
|
|
657
|
+
values,
|
|
658
|
+
originalMetadata: baselineMetadata
|
|
659
|
+
});
|
|
660
|
+
const response = await fetch(`/admin/orders/${data.id}`, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
credentials: "include",
|
|
663
|
+
headers: {
|
|
664
|
+
"Content-Type": "application/json"
|
|
665
|
+
},
|
|
666
|
+
body: JSON.stringify({
|
|
667
|
+
metadata: metadataPayload
|
|
668
|
+
})
|
|
669
|
+
});
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
const payload = await response.json().catch(() => null);
|
|
672
|
+
throw new Error((payload == null ? void 0 : payload.message) ?? "Unable to save metadata");
|
|
673
|
+
}
|
|
674
|
+
const updated = await response.json();
|
|
675
|
+
const nextMetadata = updated.order.metadata;
|
|
676
|
+
setBaselineMetadata(nextMetadata);
|
|
677
|
+
setValues(buildInitialFormState(descriptors, nextMetadata));
|
|
678
|
+
ui.toast.success("Metadata saved");
|
|
679
|
+
await queryClient.invalidateQueries({
|
|
680
|
+
queryKey: ["orders"]
|
|
681
|
+
});
|
|
682
|
+
} catch (error) {
|
|
683
|
+
ui.toast.error(error instanceof Error ? error.message : "Save failed");
|
|
684
|
+
} finally {
|
|
685
|
+
setIsSaving(false);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col gap-y-4", children: [
|
|
689
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex flex-col gap-y-1", children: [
|
|
690
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-3", children: [
|
|
691
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Metadata" }),
|
|
692
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", rounded: "full", children: descriptors.length })
|
|
693
|
+
] }),
|
|
694
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Structured metadata mapped to the keys you configured in the plugin options." })
|
|
695
|
+
] }),
|
|
696
|
+
isPending ? /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-[160px] w-full" }) : isError ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "error", label: "Configuration unavailable", children: [
|
|
697
|
+
"Unable to load metadata configuration for this plugin. Confirm that the plugin is registered with options in ",
|
|
698
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: "medusa-config.ts" }),
|
|
699
|
+
"."
|
|
700
|
+
] }) : !descriptors.length ? /* @__PURE__ */ jsxRuntime.jsxs(ui.InlineTip, { variant: "info", label: "No configured metadata keys", children: [
|
|
701
|
+
"Provide a ",
|
|
702
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: "metadataDescriptors" }),
|
|
703
|
+
" array in the plugin options to control which keys show up here.",
|
|
704
|
+
" ",
|
|
705
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
706
|
+
"a",
|
|
707
|
+
{
|
|
708
|
+
className: "text-ui-fg-interactive underline",
|
|
709
|
+
href: CONFIG_DOCS_URL$1,
|
|
710
|
+
target: "_blank",
|
|
711
|
+
rel: "noreferrer",
|
|
712
|
+
children: "Learn how to configure it."
|
|
713
|
+
}
|
|
714
|
+
)
|
|
715
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
716
|
+
/* @__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: [
|
|
717
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
718
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
719
|
+
"th",
|
|
720
|
+
{
|
|
721
|
+
scope: "col",
|
|
722
|
+
className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
|
|
723
|
+
children: "Label"
|
|
724
|
+
}
|
|
725
|
+
),
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
727
|
+
"th",
|
|
728
|
+
{
|
|
729
|
+
scope: "col",
|
|
730
|
+
className: "txt-compact-xsmall-plus text-left uppercase tracking-wide text-ui-fg-muted px-4 py-3",
|
|
731
|
+
children: "Value"
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
] }) }),
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "divide-y divide-ui-border-subtle bg-ui-bg-base", children: descriptors.map((descriptor) => {
|
|
736
|
+
const value = values[descriptor.key];
|
|
737
|
+
const error = errors[descriptor.key];
|
|
738
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
740
|
+
"th",
|
|
741
|
+
{
|
|
742
|
+
scope: "row",
|
|
743
|
+
className: "txt-compact-medium text-ui-fg-base align-top px-4 py-4",
|
|
744
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: descriptor.label ?? descriptor.key }),
|
|
746
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "txt-compact-xsmall-plus text-ui-fg-muted uppercase tracking-wide", children: descriptor.type })
|
|
747
|
+
] })
|
|
748
|
+
}
|
|
749
|
+
),
|
|
750
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "align-top px-4 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
751
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
752
|
+
ValueField$1,
|
|
753
|
+
{
|
|
754
|
+
descriptor,
|
|
755
|
+
value,
|
|
756
|
+
onStringChange: handleStringChange,
|
|
757
|
+
onBooleanChange: handleBooleanChange
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-error", children: error })
|
|
761
|
+
] }) })
|
|
762
|
+
] }, descriptor.key);
|
|
763
|
+
}) })
|
|
764
|
+
] }) }),
|
|
765
|
+
/* @__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: [
|
|
766
|
+
/* @__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." }),
|
|
767
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
768
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
769
|
+
ui.Button,
|
|
770
|
+
{
|
|
771
|
+
variant: "secondary",
|
|
772
|
+
size: "small",
|
|
773
|
+
disabled: !isDirty || isSaving,
|
|
774
|
+
onClick: handleReset,
|
|
775
|
+
children: "Reset"
|
|
776
|
+
}
|
|
777
|
+
),
|
|
778
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
779
|
+
ui.Button,
|
|
780
|
+
{
|
|
781
|
+
size: "small",
|
|
782
|
+
onClick: handleSubmit,
|
|
783
|
+
disabled: !isDirty || hasErrors || isSaving,
|
|
784
|
+
isLoading: isSaving,
|
|
785
|
+
children: "Save metadata"
|
|
786
|
+
}
|
|
787
|
+
)
|
|
788
|
+
] })
|
|
789
|
+
] })
|
|
790
|
+
] })
|
|
791
|
+
] });
|
|
792
|
+
};
|
|
793
|
+
const ValueField$1 = ({
|
|
794
|
+
descriptor,
|
|
795
|
+
value,
|
|
796
|
+
onStringChange,
|
|
797
|
+
onBooleanChange
|
|
798
|
+
}) => {
|
|
799
|
+
const fileInputRef = react.useRef(null);
|
|
800
|
+
const [isUploading, setIsUploading] = react.useState(false);
|
|
801
|
+
const handleFileUpload = async (event) => {
|
|
802
|
+
var _a;
|
|
803
|
+
const file = (_a = event.target.files) == null ? void 0 : _a[0];
|
|
804
|
+
if (!file) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
setIsUploading(true);
|
|
808
|
+
try {
|
|
809
|
+
const formData = new FormData();
|
|
810
|
+
formData.append("files", file);
|
|
811
|
+
const response = await fetch("/admin/uploads", {
|
|
812
|
+
method: "POST",
|
|
813
|
+
credentials: "include",
|
|
814
|
+
body: formData
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
const payload = await response.json().catch(() => null);
|
|
818
|
+
throw new Error((payload == null ? void 0 : payload.message) ?? "File upload failed");
|
|
819
|
+
}
|
|
820
|
+
const result = await response.json();
|
|
821
|
+
if (result.files && result.files.length > 0) {
|
|
822
|
+
const uploadedFile = result.files[0];
|
|
823
|
+
const fileUrl = uploadedFile.url || uploadedFile.key;
|
|
824
|
+
if (fileUrl) {
|
|
825
|
+
onStringChange(descriptor.key, fileUrl);
|
|
826
|
+
ui.toast.success("File uploaded successfully");
|
|
827
|
+
} else {
|
|
828
|
+
throw new Error("File upload succeeded but no URL returned");
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
throw new Error("File upload failed - no files returned");
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
ui.toast.error(
|
|
835
|
+
error instanceof Error ? error.message : "Failed to upload file"
|
|
836
|
+
);
|
|
837
|
+
} finally {
|
|
838
|
+
setIsUploading(false);
|
|
839
|
+
if (fileInputRef.current) {
|
|
840
|
+
fileInputRef.current.value = "";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
if (descriptor.type === "bool") {
|
|
845
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
846
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
847
|
+
ui.Switch,
|
|
848
|
+
{
|
|
849
|
+
checked: Boolean(value),
|
|
850
|
+
onCheckedChange: (checked) => onBooleanChange(descriptor.key, Boolean(checked)),
|
|
851
|
+
"aria-label": `Toggle ${descriptor.label ?? descriptor.key}`
|
|
852
|
+
}
|
|
853
|
+
),
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "txt-compact-small text-ui-fg-muted", children: Boolean(value) ? "True" : "False" })
|
|
855
|
+
] });
|
|
856
|
+
}
|
|
857
|
+
if (descriptor.type === "text") {
|
|
858
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
859
|
+
ui.Textarea,
|
|
860
|
+
{
|
|
861
|
+
value: value ?? "",
|
|
862
|
+
placeholder: "Enter text",
|
|
863
|
+
rows: 3,
|
|
864
|
+
onChange: (event) => onStringChange(descriptor.key, event.target.value)
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (descriptor.type === "number") {
|
|
869
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
870
|
+
ui.Input,
|
|
871
|
+
{
|
|
872
|
+
type: "text",
|
|
873
|
+
inputMode: "decimal",
|
|
874
|
+
placeholder: "0.00",
|
|
875
|
+
value: value ?? "",
|
|
876
|
+
onChange: (event) => onStringChange(descriptor.key, event.target.value)
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
881
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
882
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
883
|
+
ui.Input,
|
|
884
|
+
{
|
|
885
|
+
type: "url",
|
|
886
|
+
placeholder: "https://example.com/file",
|
|
887
|
+
value: value ?? "",
|
|
888
|
+
onChange: (event) => onStringChange(descriptor.key, event.target.value),
|
|
889
|
+
className: "flex-1"
|
|
890
|
+
}
|
|
891
|
+
),
|
|
892
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
893
|
+
"input",
|
|
894
|
+
{
|
|
895
|
+
ref: fileInputRef,
|
|
896
|
+
type: "file",
|
|
897
|
+
className: "hidden",
|
|
898
|
+
onChange: handleFileUpload,
|
|
899
|
+
disabled: isUploading,
|
|
900
|
+
"aria-label": `Upload file for ${descriptor.label ?? descriptor.key}`
|
|
901
|
+
}
|
|
902
|
+
),
|
|
903
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
904
|
+
ui.Button,
|
|
905
|
+
{
|
|
906
|
+
type: "button",
|
|
907
|
+
variant: "secondary",
|
|
908
|
+
size: "small",
|
|
909
|
+
onClick: () => {
|
|
910
|
+
var _a;
|
|
911
|
+
return (_a = fileInputRef.current) == null ? void 0 : _a.click();
|
|
912
|
+
},
|
|
913
|
+
disabled: isUploading,
|
|
914
|
+
isLoading: isUploading,
|
|
915
|
+
children: isUploading ? "Uploading..." : "Upload"
|
|
916
|
+
}
|
|
917
|
+
)
|
|
918
|
+
] }),
|
|
919
|
+
typeof value === "string" && value && /* @__PURE__ */ jsxRuntime.jsx(
|
|
920
|
+
"a",
|
|
921
|
+
{
|
|
922
|
+
className: "txt-compact-small-plus text-ui-fg-interactive underline",
|
|
923
|
+
href: value,
|
|
924
|
+
target: "_blank",
|
|
925
|
+
rel: "noreferrer",
|
|
926
|
+
children: "View file"
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
] });
|
|
930
|
+
};
|
|
931
|
+
adminSdk.defineWidgetConfig({
|
|
932
|
+
zone: "order.details.after"
|
|
933
|
+
});
|
|
596
934
|
const CONFIG_DOCS_URL = "https://docs.medusajs.com/admin/extension-points/widgets#product-details";
|
|
597
935
|
const ProductMetadataTableWidget = ({ data }) => {
|
|
598
936
|
const { data: descriptors = [], isPending, isError } = useProductMetadataConfig();
|
|
@@ -1067,6 +1405,10 @@ const widgetModule = { widgets: [
|
|
|
1067
1405
|
Component: HideDefaultMetadataWidget,
|
|
1068
1406
|
zone: ["product.details.side.before"]
|
|
1069
1407
|
},
|
|
1408
|
+
{
|
|
1409
|
+
Component: OrderMetadataTableWidget,
|
|
1410
|
+
zone: ["order.details.after"]
|
|
1411
|
+
},
|
|
1070
1412
|
{
|
|
1071
1413
|
Component: ProductMetadataTableWidget,
|
|
1072
1414
|
zone: ["product.details.after"]
|