@empty-complete-org/medusa-product-attributes 0.12.2 → 0.14.0

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,7 +1,8 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import { defineWidgetConfig } from "@medusajs/admin-sdk";
2
+ import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
3
3
  import { Container, Heading, Button, Text, Badge, Input } from "@medusajs/ui";
4
4
  import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
5
+ import * as React from "react";
5
6
  import { useState, useRef, useEffect } from "react";
6
7
  import Medusa from "@medusajs/js-sdk";
7
8
  const sdk = new Medusa({
@@ -11,17 +12,37 @@ const sdk = new Medusa({
11
12
  type: "session"
12
13
  }
13
14
  });
14
- const emptyForm = () => ({ label: "", type: "text", unit: "" });
15
+ const emptyForm$1 = () => ({ label: "", type: "text", unit: "" });
15
16
  const CategoryAttributeTemplatesWidget = ({
16
17
  data
17
18
  }) => {
19
+ var _a, _b;
18
20
  const categoryId = data.id;
19
21
  const qc = useQueryClient();
20
22
  const queryKey = ["category-custom-attributes", categoryId];
21
23
  const [showAddForm, setShowAddForm] = useState(false);
22
- const [addForm, setAddForm] = useState(emptyForm());
24
+ const [showPresetList, setShowPresetList] = useState(false);
25
+ const [addForm, setAddForm] = useState(emptyForm$1());
23
26
  const [confirmDeleteId, setConfirmDeleteId] = useState(null);
24
27
  const [mutationError, setMutationError] = useState(null);
28
+ const presetsQuery = useQuery({
29
+ queryKey: ["attribute-presets"],
30
+ queryFn: () => sdk.client.fetch(`/admin/attribute-presets`),
31
+ enabled: showPresetList
32
+ });
33
+ const applyPresetMutation = useMutation({
34
+ mutationFn: (presetId) => sdk.client.fetch(`/admin/attribute-presets/${presetId}/apply`, {
35
+ method: "POST",
36
+ body: { category_id: categoryId }
37
+ }),
38
+ onSuccess: () => {
39
+ qc.invalidateQueries({ queryKey });
40
+ setShowPresetList(false);
41
+ },
42
+ onError: (err) => {
43
+ setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении пресета");
44
+ }
45
+ });
25
46
  const {
26
47
  data: result,
27
48
  isLoading,
@@ -41,7 +62,7 @@ const CategoryAttributeTemplatesWidget = ({
41
62
  onSuccess: () => {
42
63
  qc.invalidateQueries({ queryKey });
43
64
  setShowAddForm(false);
44
- setAddForm(emptyForm());
65
+ setAddForm(emptyForm$1());
45
66
  setMutationError(null);
46
67
  },
47
68
  onError: (err) => {
@@ -66,19 +87,62 @@ const CategoryAttributeTemplatesWidget = ({
66
87
  unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
67
88
  });
68
89
  };
69
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
90
+ const typeLabel2 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
70
91
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
71
92
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
72
93
  /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
73
- !showAddForm && /* @__PURE__ */ jsx(
74
- Button,
94
+ !showAddForm && !showPresetList && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
95
+ /* @__PURE__ */ jsx(
96
+ Button,
97
+ {
98
+ variant: "secondary",
99
+ size: "small",
100
+ onClick: () => setShowPresetList(true),
101
+ children: "Из пресета"
102
+ }
103
+ ),
104
+ /* @__PURE__ */ jsx(
105
+ Button,
106
+ {
107
+ variant: "secondary",
108
+ size: "small",
109
+ onClick: () => setShowAddForm(true),
110
+ children: "+ Добавить"
111
+ }
112
+ )
113
+ ] })
114
+ ] }),
115
+ showPresetList && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3", children: [
116
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
117
+ /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Выберите пресет" }),
118
+ /* @__PURE__ */ jsx(
119
+ Button,
120
+ {
121
+ size: "small",
122
+ variant: "secondary",
123
+ onClick: () => setShowPresetList(false),
124
+ children: "Закрыть"
125
+ }
126
+ )
127
+ ] }),
128
+ presetsQuery.isLoading && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }),
129
+ ((_a = presetsQuery.data) == null ? void 0 : _a.attribute_presets.length) === 0 && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
130
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: (_b = presetsQuery.data) == null ? void 0 : _b.attribute_presets.map((p) => /* @__PURE__ */ jsxs(
131
+ "button",
75
132
  {
76
- variant: "secondary",
77
- size: "small",
78
- onClick: () => setShowAddForm(true),
79
- children: "+ Добавить"
80
- }
81
- )
133
+ onClick: () => applyPresetMutation.mutate(p.id),
134
+ disabled: applyPresetMutation.isPending,
135
+ className: "flex items-center justify-between rounded border border-ui-border-base px-3 py-2 text-left text-sm hover:bg-ui-bg-subtle disabled:opacity-50",
136
+ children: [
137
+ /* @__PURE__ */ jsx("span", { children: p.label }),
138
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
139
+ typeLabel2(p.type),
140
+ p.unit ? `, ${p.unit}` : ""
141
+ ] })
142
+ ]
143
+ },
144
+ p.id
145
+ )) })
82
146
  ] }),
83
147
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
84
148
  isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
@@ -91,7 +155,7 @@ const CategoryAttributeTemplatesWidget = ({
91
155
  children: [
92
156
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
93
157
  /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
94
- typeLabel(attr.type),
158
+ typeLabel2(attr.type),
95
159
  attr.unit ? `, ${attr.unit}` : ""
96
160
  ] }),
97
161
  /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
@@ -142,7 +206,7 @@ const CategoryAttributeTemplatesWidget = ({
142
206
  children: [
143
207
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
144
208
  /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
145
- typeLabel(attr.type),
209
+ typeLabel2(attr.type),
146
210
  attr.unit ? `, ${attr.unit}` : ""
147
211
  ] }),
148
212
  /* @__PURE__ */ jsx(
@@ -214,7 +278,7 @@ const CategoryAttributeTemplatesWidget = ({
214
278
  size: "small",
215
279
  onClick: () => {
216
280
  setShowAddForm(false);
217
- setAddForm(emptyForm());
281
+ setAddForm(emptyForm$1());
218
282
  setMutationError(null);
219
283
  },
220
284
  children: "Отмена"
@@ -232,6 +296,7 @@ const ProductAttributeValuesWidget = ({
232
296
  }) => {
233
297
  var _a, _b, _c;
234
298
  const productId = data.id;
299
+ const productHandle = data.handle || productId;
235
300
  const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
236
301
  const qc = useQueryClient();
237
302
  const [formValues, setFormValues] = useState({});
@@ -282,12 +347,20 @@ const ProductAttributeValuesWidget = ({
282
347
  const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
283
348
  saveMutation.mutate({ attributes: attributesToUpdate });
284
349
  };
285
- const handleFileUpload = async (attrId, file) => {
350
+ const handleFileUpload = async (attr, file) => {
286
351
  var _a2, _b2;
352
+ const attrId = attr.id;
287
353
  setUploadingId(attrId);
288
354
  try {
355
+ const dot = file.name.lastIndexOf(".");
356
+ const ext = dot > -1 ? file.name.slice(dot) : "";
357
+ const renamed = new File(
358
+ [file],
359
+ `${productHandle}_${attr.key}${ext}`,
360
+ { type: file.type }
361
+ );
289
362
  const formData = new FormData();
290
- formData.append("files", file);
363
+ formData.append("files", renamed);
291
364
  const response = await fetch(`/admin/uploads`, {
292
365
  method: "POST",
293
366
  credentials: "include",
@@ -365,7 +438,7 @@ const ProductAttributeValuesWidget = ({
365
438
  onChange: (e) => {
366
439
  var _a2;
367
440
  const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
368
- if (f) handleFileUpload(attr.id, f);
441
+ if (f) handleFileUpload(attr, f);
369
442
  }
370
443
  }
371
444
  ),
@@ -427,6 +500,269 @@ const ProductAttributeValuesWidget = ({
427
500
  defineWidgetConfig({
428
501
  zone: "product.details.after"
429
502
  });
503
+ var __defProp = Object.defineProperty;
504
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
505
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
506
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
507
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
508
+ var __spreadValues = (a, b) => {
509
+ for (var prop in b || (b = {}))
510
+ if (__hasOwnProp.call(b, prop))
511
+ __defNormalProp(a, prop, b[prop]);
512
+ if (__getOwnPropSymbols)
513
+ for (var prop of __getOwnPropSymbols(b)) {
514
+ if (__propIsEnum.call(b, prop))
515
+ __defNormalProp(a, prop, b[prop]);
516
+ }
517
+ return a;
518
+ };
519
+ var __objRest = (source, exclude) => {
520
+ var target = {};
521
+ for (var prop in source)
522
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
523
+ target[prop] = source[prop];
524
+ if (source != null && __getOwnPropSymbols)
525
+ for (var prop of __getOwnPropSymbols(source)) {
526
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
527
+ target[prop] = source[prop];
528
+ }
529
+ return target;
530
+ };
531
+ const CogSixTooth = React.forwardRef(
532
+ (_a, ref) => {
533
+ var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
534
+ return /* @__PURE__ */ React.createElement(
535
+ "svg",
536
+ __spreadValues({
537
+ xmlns: "http://www.w3.org/2000/svg",
538
+ width: 15,
539
+ height: 15,
540
+ viewBox: "0 0 15 15",
541
+ fill: "none",
542
+ ref
543
+ }, props),
544
+ /* @__PURE__ */ React.createElement(
545
+ "g",
546
+ {
547
+ stroke: color,
548
+ strokeLinecap: "round",
549
+ strokeLinejoin: "round",
550
+ strokeWidth: 1.5,
551
+ clipPath: "url(#a)"
552
+ },
553
+ /* @__PURE__ */ React.createElement("path", { d: "M7.5 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4" }),
554
+ /* @__PURE__ */ React.createElement("path", { d: "m12.989 5.97-.826-.292a5 5 0 0 0-.323-.685 5 5 0 0 0-.43-.621l.16-.86a1.43 1.43 0 0 0-.692-1.503l-.312-.18a1.43 1.43 0 0 0-1.647.152l-.663.566a5 5 0 0 0-1.513 0L6.08 1.98a1.43 1.43 0 0 0-1.647-.152l-.312.18a1.43 1.43 0 0 0-.691 1.503l.16.857c-.32.4-.574.841-.758 1.31l-.82.29a1.43 1.43 0 0 0-.956 1.35v.36c0 .608.383 1.15.955 1.35l.826.292c.09.232.194.462.323.684.128.222.275.427.43.622l-.16.86c-.111.597.166 1.2.691 1.503l.312.18a1.43 1.43 0 0 0 1.647-.152l.663-.567a5 5 0 0 0 1.512 0l.663.568a1.43 1.43 0 0 0 1.647.152l.312-.18c.526-.304.803-.906.691-1.502l-.16-.86c.32-.398.575-.84.757-1.308l.822-.29c.572-.202.956-.743.956-1.35v-.36c0-.608-.383-1.149-.956-1.35z" })
555
+ ),
556
+ /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("clipPath", { id: "a" }, /* @__PURE__ */ React.createElement("path", { fill: "#fff", d: "M0 0h15v15H0z" })))
557
+ );
558
+ }
559
+ );
560
+ CogSixTooth.displayName = "CogSixTooth";
561
+ const emptyForm = () => ({
562
+ label: "",
563
+ type: "text",
564
+ unit: "",
565
+ description: ""
566
+ });
567
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
568
+ const ProductAttributesSettingsPage = () => {
569
+ const qc = useQueryClient();
570
+ const queryKey = ["attribute-presets"];
571
+ const [showAddForm, setShowAddForm] = useState(false);
572
+ const [addForm, setAddForm] = useState(emptyForm());
573
+ const [confirmDeleteId, setConfirmDeleteId] = useState(null);
574
+ const { data, isLoading, isError } = useQuery({
575
+ queryKey,
576
+ queryFn: () => sdk.client.fetch(`/admin/attribute-presets`)
577
+ });
578
+ const presets = (data == null ? void 0 : data.attribute_presets) ?? [];
579
+ const createMutation = useMutation({
580
+ mutationFn: (body) => sdk.client.fetch(`/admin/attribute-presets`, {
581
+ method: "POST",
582
+ body: {
583
+ label: body.label,
584
+ type: body.type,
585
+ unit: body.type === "number" && body.unit ? body.unit : null,
586
+ description: body.description || null
587
+ }
588
+ }),
589
+ onSuccess: () => {
590
+ qc.invalidateQueries({ queryKey });
591
+ setShowAddForm(false);
592
+ setAddForm(emptyForm());
593
+ }
594
+ });
595
+ const deleteMutation = useMutation({
596
+ mutationFn: (id) => sdk.client.fetch(`/admin/attribute-presets`, {
597
+ method: "PATCH",
598
+ body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
599
+ }),
600
+ onSuccess: () => {
601
+ qc.invalidateQueries({ queryKey });
602
+ setConfirmDeleteId(null);
603
+ }
604
+ });
605
+ const handleAdd = () => {
606
+ if (!addForm.label.trim()) return;
607
+ createMutation.mutate({
608
+ ...addForm,
609
+ label: addForm.label.trim(),
610
+ unit: addForm.unit.trim(),
611
+ description: addForm.description.trim()
612
+ });
613
+ };
614
+ return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
615
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
616
+ /* @__PURE__ */ jsxs("div", { children: [
617
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Пресеты атрибутов" }),
618
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Глобальные шаблоны атрибутов, которые можно применить к любой категории." })
619
+ ] }),
620
+ !showAddForm && /* @__PURE__ */ jsx(
621
+ Button,
622
+ {
623
+ variant: "secondary",
624
+ size: "small",
625
+ onClick: () => setShowAddForm(true),
626
+ children: "+ Добавить пресет"
627
+ }
628
+ )
629
+ ] }),
630
+ isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
631
+ isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить пресеты." }) }),
632
+ !isLoading && !isError && presets.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов пока нет. Добавьте первый." }) }),
633
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: presets.map(
634
+ (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs(
635
+ "div",
636
+ {
637
+ className: "flex items-center gap-3 px-6 py-3 text-sm",
638
+ children: [
639
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
640
+ "Удалить «",
641
+ p.label,
642
+ "»?"
643
+ ] }),
644
+ /* @__PURE__ */ jsx(
645
+ Button,
646
+ {
647
+ size: "small",
648
+ variant: "danger",
649
+ onClick: () => deleteMutation.mutate(p.id),
650
+ isLoading: deleteMutation.isPending,
651
+ children: "Удалить"
652
+ }
653
+ ),
654
+ /* @__PURE__ */ jsx(
655
+ Button,
656
+ {
657
+ size: "small",
658
+ variant: "secondary",
659
+ onClick: () => setConfirmDeleteId(null),
660
+ children: "Отмена"
661
+ }
662
+ )
663
+ ]
664
+ },
665
+ p.id
666
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
667
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
668
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
669
+ /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: p.label }),
670
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
671
+ typeLabel(p.type),
672
+ p.unit ? `, ${p.unit}` : ""
673
+ ] })
674
+ ] }),
675
+ p.description && /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
676
+ ] }),
677
+ /* @__PURE__ */ jsx(
678
+ "button",
679
+ {
680
+ onClick: () => setConfirmDeleteId(p.id),
681
+ className: "text-xs text-ui-fg-error hover:underline",
682
+ children: "Удалить"
683
+ }
684
+ )
685
+ ] }, p.id)
686
+ ) }),
687
+ showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
688
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
689
+ /* @__PURE__ */ jsx(
690
+ Input,
691
+ {
692
+ value: addForm.label,
693
+ onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
694
+ placeholder: "Название (например, Сертификат)",
695
+ className: "flex-1 h-8 text-sm",
696
+ autoFocus: true
697
+ }
698
+ ),
699
+ /* @__PURE__ */ jsxs(
700
+ "select",
701
+ {
702
+ value: addForm.type,
703
+ onChange: (e) => setAddForm((f) => ({
704
+ ...f,
705
+ type: e.target.value,
706
+ unit: e.target.value === "number" ? f.unit : ""
707
+ })),
708
+ className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
709
+ children: [
710
+ /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
711
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
712
+ /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
713
+ /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
714
+ ]
715
+ }
716
+ ),
717
+ addForm.type === "number" && /* @__PURE__ */ jsx(
718
+ Input,
719
+ {
720
+ value: addForm.unit,
721
+ onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
722
+ placeholder: "ед. (кг, м...)",
723
+ className: "w-28 h-8 text-sm"
724
+ }
725
+ )
726
+ ] }),
727
+ /* @__PURE__ */ jsx(
728
+ Input,
729
+ {
730
+ value: addForm.description,
731
+ onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
732
+ placeholder: "Описание (необязательно)",
733
+ className: "h-8 text-sm"
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
737
+ /* @__PURE__ */ jsx(
738
+ Button,
739
+ {
740
+ variant: "secondary",
741
+ size: "small",
742
+ onClick: () => {
743
+ setShowAddForm(false);
744
+ setAddForm(emptyForm());
745
+ },
746
+ children: "Отмена"
747
+ }
748
+ ),
749
+ /* @__PURE__ */ jsx(
750
+ Button,
751
+ {
752
+ size: "small",
753
+ onClick: handleAdd,
754
+ isLoading: createMutation.isPending,
755
+ children: "Создать"
756
+ }
757
+ )
758
+ ] })
759
+ ] })
760
+ ] });
761
+ };
762
+ const config = defineRouteConfig({
763
+ label: "Product Attributes",
764
+ icon: CogSixTooth
765
+ });
430
766
  const widgetModule = { widgets: [
431
767
  {
432
768
  Component: CategoryAttributeTemplatesWidget,
@@ -438,10 +774,24 @@ const widgetModule = { widgets: [
438
774
  }
439
775
  ] };
440
776
  const routeModule = {
441
- routes: []
777
+ routes: [
778
+ {
779
+ Component: ProductAttributesSettingsPage,
780
+ path: "/settings/product-attributes"
781
+ }
782
+ ]
442
783
  };
443
784
  const menuItemModule = {
444
- menuItems: []
785
+ menuItems: [
786
+ {
787
+ label: config.label,
788
+ icon: config.icon,
789
+ path: "/settings/product-attributes",
790
+ nested: void 0,
791
+ rank: void 0,
792
+ translationNs: void 0
793
+ }
794
+ ]
445
795
  };
446
796
  const formModule = { customFields: {} };
447
797
  const displayModule = {
@@ -0,0 +1,2 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ export declare function POST(req: MedusaRequest, res: MedusaResponse): Promise<void>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = POST;
4
+ const product_attributes_1 = require("../../../../../modules/product-attributes");
5
+ async function POST(req, res) {
6
+ const { id } = req.params;
7
+ const { category_id } = req.body;
8
+ const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
9
+ const category_custom_attribute = await service.applyPresetToCategory(id, category_id);
10
+ res.status(201).json({ category_custom_attribute });
11
+ }
12
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../../../../../src/api/admin/attribute-presets/[id]/apply/route.ts"],"names":[],"mappings":";;AAIA,oBAMC;AATD,kFAAmF;AAG5E,KAAK,UAAU,IAAI,CAAC,GAAkB,EAAE,GAAmB;IAChE,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;IACzB,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC,IAA+B,CAAA;IAC3D,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAClF,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,EAAE,EAAE,WAAW,CAAC,CAAA;IACtF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,yBAAyB,EAAE,CAAC,CAAA;AACrD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
2
+ export declare function GET(req: MedusaRequest, res: MedusaResponse): Promise<void>;
3
+ export declare function POST(req: MedusaRequest, res: MedusaResponse): Promise<void>;
4
+ export declare function PATCH(req: MedusaRequest, res: MedusaResponse): Promise<void>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GET = GET;
4
+ exports.POST = POST;
5
+ exports.PATCH = PATCH;
6
+ const product_attributes_1 = require("../../../modules/product-attributes");
7
+ async function GET(req, res) {
8
+ const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
9
+ const attribute_presets = await service.listPresets();
10
+ res.json({ attribute_presets });
11
+ }
12
+ async function POST(req, res) {
13
+ const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
14
+ const body = req.body;
15
+ const attribute_preset = await service.createPreset(body);
16
+ res.status(201).json({ attribute_preset });
17
+ }
18
+ async function PATCH(req, res) {
19
+ const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
20
+ const { id, ...data } = req.body;
21
+ const attribute_preset = await service.updatePreset(id, data);
22
+ res.json({ attribute_preset });
23
+ }
24
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../../../src/api/admin/attribute-presets/route.ts"],"names":[],"mappings":";;AAIA,kBAIC;AAED,oBAUC;AAED,sBAYC;AAjCD,4EAA6E;AAGtE,KAAK,UAAU,GAAG,CAAC,GAAkB,EAAE,GAAmB;IAC/D,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAClF,MAAM,iBAAiB,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAA;IACrD,GAAG,CAAC,IAAI,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAA;AACjC,CAAC;AAEM,KAAK,UAAU,IAAI,CAAC,GAAkB,EAAE,GAAmB;IAChE,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAClF,MAAM,IAAI,GAAG,GAAG,CAAC,IAKhB,CAAA;IACD,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IACzD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAC5C,CAAC;AAEM,KAAK,UAAU,KAAK,CAAC,GAAkB,EAAE,GAAmB;IACjE,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAClF,MAAM,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC,IAO3B,CAAA;IACD,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAC7D,GAAG,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAChC,CAAC"}
@@ -30,6 +30,20 @@ declare const _default: import("@medusajs/types").ModuleExports<typeof CustomAtt
30
30
  primaryKey: "id";
31
31
  };
32
32
  };
33
+ attributePreset: {
34
+ id: {
35
+ serviceName: "customAttributeModule";
36
+ field: "attributePreset";
37
+ linkable: "attribute_preset_id";
38
+ primaryKey: "id";
39
+ };
40
+ toJSON: () => {
41
+ serviceName: "customAttributeModule";
42
+ field: "attributePreset";
43
+ linkable: "attribute_preset_id";
44
+ primaryKey: "id";
45
+ };
46
+ };
33
47
  };
34
48
  };
35
49
  export default _default;
@@ -0,0 +1,5 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ export declare class Migration20260406120000 extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Migration20260406120000 = void 0;
4
+ const migrations_1 = require("@mikro-orm/migrations");
5
+ class Migration20260406120000 extends migrations_1.Migration {
6
+ async up() {
7
+ this.addSql(`
8
+ CREATE TABLE IF NOT EXISTS "attribute_preset" (
9
+ "id" text NOT NULL,
10
+ "key" text NOT NULL,
11
+ "label" text NOT NULL,
12
+ "type" text NOT NULL DEFAULT 'text',
13
+ "unit" text NULL,
14
+ "description" text NULL,
15
+ "created_at" timestamptz NOT NULL DEFAULT now(),
16
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
17
+ "deleted_at" timestamptz NULL,
18
+ CONSTRAINT "attribute_preset_pkey" PRIMARY KEY ("id")
19
+ );
20
+ `);
21
+ this.addSql(`
22
+ CREATE INDEX IF NOT EXISTS "IDX_attribute_preset_deleted_at"
23
+ ON "attribute_preset" ("deleted_at")
24
+ WHERE "deleted_at" IS NOT NULL;
25
+ `);
26
+ }
27
+ async down() {
28
+ this.addSql(`DROP TABLE IF EXISTS "attribute_preset" CASCADE;`);
29
+ }
30
+ }
31
+ exports.Migration20260406120000 = Migration20260406120000;
32
+ //# sourceMappingURL=Migration20260406120000.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Migration20260406120000.js","sourceRoot":"","sources":["../../../../../../src/modules/product-attributes/migrations/Migration20260406120000.ts"],"names":[],"mappings":";;;AAAA,sDAAiD;AAEjD,MAAa,uBAAwB,SAAQ,sBAAS;IAC3C,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAA;IACjE,CAAC;CACF;AA3BD,0DA2BC"}
@@ -0,0 +1,9 @@
1
+ declare const AttributePreset: import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder<{
2
+ id: import("@medusajs/framework/utils").PrimaryKeyModifier<string, import("@medusajs/framework/utils").IdProperty>;
3
+ key: import("@medusajs/framework/utils").TextProperty;
4
+ label: import("@medusajs/framework/utils").TextProperty;
5
+ type: import("@medusajs/framework/utils").TextProperty;
6
+ unit: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
7
+ description: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
8
+ }>, "attribute_preset">;
9
+ export default AttributePreset;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@medusajs/framework/utils");
4
+ const AttributePreset = utils_1.model.define("attribute_preset", {
5
+ id: utils_1.model.id().primaryKey(),
6
+ key: utils_1.model.text(),
7
+ label: utils_1.model.text(),
8
+ type: utils_1.model.text().default("text"),
9
+ unit: utils_1.model.text().nullable(),
10
+ description: utils_1.model.text().nullable(),
11
+ });
12
+ exports.default = AttributePreset;
13
+ //# sourceMappingURL=attribute-preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attribute-preset.js","sourceRoot":"","sources":["../../../../../../src/modules/product-attributes/models/attribute-preset.ts"],"names":[],"mappings":";;AAAA,qDAAiD;AAEjD,MAAM,eAAe,GAAG,aAAK,CAAC,MAAM,CAAC,kBAAkB,EAAE;IACvD,EAAE,EAAE,aAAK,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE;IAC3B,GAAG,EAAE,aAAK,CAAC,IAAI,EAAE;IACjB,KAAK,EAAE,aAAK,CAAC,IAAI,EAAE;IACnB,IAAI,EAAE,aAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,aAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC7B,WAAW,EAAE,aAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAA;AAEF,kBAAe,eAAe,CAAA"}