@empty-complete-org/medusa-product-attributes 0.11.0 → 0.12.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.
@@ -14,7 +14,7 @@ const sdk = new Medusa__default.default({
14
14
  type: "session"
15
15
  }
16
16
  });
17
- const emptyForm = () => ({ label: "", type: "text" });
17
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
18
18
  const CategoryAttributeTemplatesWidget = ({
19
19
  data
20
20
  }) => {
@@ -34,6 +34,8 @@ const CategoryAttributeTemplatesWidget = ({
34
34
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
35
35
  });
36
36
  const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
37
+ const ownAttributes = attributes.filter((a) => !a.inherited);
38
+ const inheritedAttributes = attributes.filter((a) => a.inherited);
37
39
  const createMutation = reactQuery.useMutation({
38
40
  mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
39
41
  method: "POST",
@@ -63,9 +65,11 @@ const CategoryAttributeTemplatesWidget = ({
63
65
  if (!addForm.label.trim()) return;
64
66
  createMutation.mutate({
65
67
  label: addForm.label.trim(),
66
- type: addForm.type
68
+ type: addForm.type,
69
+ unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
67
70
  });
68
71
  };
72
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
69
73
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
70
74
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
71
75
  /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
@@ -81,60 +85,81 @@ const CategoryAttributeTemplatesWidget = ({
81
85
  ] }),
82
86
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
83
87
  isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
84
- !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
85
- attributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes.map(
86
- (attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxRuntime.jsxs(
87
- "div",
88
- {
89
- className: "flex items-center gap-3 px-6 py-3 text-sm",
90
- children: [
91
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
92
- "Удалить «",
93
- attr.label,
94
- "»?"
95
- ] }),
96
- /* @__PURE__ */ jsxRuntime.jsx(
97
- ui.Button,
98
- {
99
- size: "small",
100
- variant: "danger",
101
- onClick: () => deleteMutation.mutate(attr.id),
102
- isLoading: deleteMutation.isPending,
103
- children: "Удалить"
104
- }
105
- ),
106
- /* @__PURE__ */ jsxRuntime.jsx(
107
- ui.Button,
108
- {
109
- size: "small",
110
- variant: "secondary",
111
- onClick: () => setConfirmDeleteId(null),
112
- children: "Отмена"
113
- }
114
- )
115
- ]
116
- },
117
- attr.id
118
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
88
+ inheritedAttributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
89
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Унаследованные" }) }),
90
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: inheritedAttributes.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
119
91
  "div",
120
92
  {
121
93
  className: "flex items-center gap-3 px-6 py-3",
122
94
  children: [
123
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
124
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: attr.type === "text" ? "Текст" : attr.type === "number" ? "Число" : attr.type }),
125
- /* @__PURE__ */ jsxRuntime.jsx(
126
- "button",
127
- {
128
- onClick: () => setConfirmDeleteId(attr.id),
129
- className: "text-xs text-ui-fg-error hover:underline",
130
- children: "Удалить"
131
- }
132
- )
95
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
96
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
97
+ typeLabel(attr.type),
98
+ attr.unit ? `, ${attr.unit}` : ""
99
+ ] }),
100
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
133
101
  ]
134
102
  },
135
103
  attr.id
136
- )
137
- ) }),
104
+ )) })
105
+ ] }),
106
+ ownAttributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
107
+ inheritedAttributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Свои" }) }),
108
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: ownAttributes.map(
109
+ (attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxRuntime.jsxs(
110
+ "div",
111
+ {
112
+ className: "flex items-center gap-3 px-6 py-3 text-sm",
113
+ children: [
114
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
115
+ "Удалить «",
116
+ attr.label,
117
+ "»?"
118
+ ] }),
119
+ /* @__PURE__ */ jsxRuntime.jsx(
120
+ ui.Button,
121
+ {
122
+ size: "small",
123
+ variant: "danger",
124
+ onClick: () => deleteMutation.mutate(attr.id),
125
+ isLoading: deleteMutation.isPending,
126
+ children: "Удалить"
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsxRuntime.jsx(
130
+ ui.Button,
131
+ {
132
+ size: "small",
133
+ variant: "secondary",
134
+ onClick: () => setConfirmDeleteId(null),
135
+ children: "Отмена"
136
+ }
137
+ )
138
+ ]
139
+ },
140
+ attr.id
141
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
142
+ "div",
143
+ {
144
+ className: "flex items-center gap-3 px-6 py-3",
145
+ children: [
146
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
147
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
148
+ /* @__PURE__ */ jsxRuntime.jsx(
149
+ "button",
150
+ {
151
+ onClick: () => setConfirmDeleteId(attr.id),
152
+ className: "text-xs text-ui-fg-error hover:underline",
153
+ children: "Удалить"
154
+ }
155
+ )
156
+ ]
157
+ },
158
+ attr.id
159
+ )
160
+ ) })
161
+ ] }),
162
+ !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
138
163
  showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
139
164
  /* @__PURE__ */ jsxRuntime.jsx(
140
165
  ui.Input,
@@ -152,15 +177,27 @@ const CategoryAttributeTemplatesWidget = ({
152
177
  value: addForm.type,
153
178
  onChange: (e) => setAddForm((f) => ({
154
179
  ...f,
155
- type: e.target.value
180
+ type: e.target.value,
181
+ unit: e.target.value === "number" ? f.unit : ""
156
182
  })),
157
183
  className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
158
184
  children: [
159
185
  /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
160
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" })
186
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
187
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
188
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
161
189
  ]
162
190
  }
163
191
  ),
192
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
193
+ ui.Input,
194
+ {
195
+ value: addForm.unit,
196
+ onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
197
+ placeholder: "ед. (кг, м, шт...)",
198
+ className: "w-28 h-8 text-sm"
199
+ }
200
+ ),
164
201
  /* @__PURE__ */ jsxRuntime.jsx(
165
202
  ui.Button,
166
203
  {
@@ -11,7 +11,7 @@ const sdk = new Medusa({
11
11
  type: "session"
12
12
  }
13
13
  });
14
- const emptyForm = () => ({ label: "", type: "text" });
14
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
15
15
  const CategoryAttributeTemplatesWidget = ({
16
16
  data
17
17
  }) => {
@@ -31,6 +31,8 @@ const CategoryAttributeTemplatesWidget = ({
31
31
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
32
32
  });
33
33
  const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
34
+ const ownAttributes = attributes.filter((a) => !a.inherited);
35
+ const inheritedAttributes = attributes.filter((a) => a.inherited);
34
36
  const createMutation = useMutation({
35
37
  mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
36
38
  method: "POST",
@@ -60,9 +62,11 @@ const CategoryAttributeTemplatesWidget = ({
60
62
  if (!addForm.label.trim()) return;
61
63
  createMutation.mutate({
62
64
  label: addForm.label.trim(),
63
- type: addForm.type
65
+ type: addForm.type,
66
+ unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
64
67
  });
65
68
  };
69
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
66
70
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
67
71
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
68
72
  /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
@@ -78,60 +82,81 @@ const CategoryAttributeTemplatesWidget = ({
78
82
  ] }),
79
83
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
80
84
  isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
81
- !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
82
- attributes.length > 0 && /* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes.map(
83
- (attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxs(
84
- "div",
85
- {
86
- className: "flex items-center gap-3 px-6 py-3 text-sm",
87
- children: [
88
- /* @__PURE__ */ jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
89
- "Удалить «",
90
- attr.label,
91
- "»?"
92
- ] }),
93
- /* @__PURE__ */ jsx(
94
- Button,
95
- {
96
- size: "small",
97
- variant: "danger",
98
- onClick: () => deleteMutation.mutate(attr.id),
99
- isLoading: deleteMutation.isPending,
100
- children: "Удалить"
101
- }
102
- ),
103
- /* @__PURE__ */ jsx(
104
- Button,
105
- {
106
- size: "small",
107
- variant: "secondary",
108
- onClick: () => setConfirmDeleteId(null),
109
- children: "Отмена"
110
- }
111
- )
112
- ]
113
- },
114
- attr.id
115
- ) : /* @__PURE__ */ jsxs(
85
+ inheritedAttributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
86
+ /* @__PURE__ */ jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Унаследованные" }) }),
87
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: inheritedAttributes.map((attr) => /* @__PURE__ */ jsxs(
116
88
  "div",
117
89
  {
118
90
  className: "flex items-center gap-3 px-6 py-3",
119
91
  children: [
120
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
121
- /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: attr.type === "text" ? "Текст" : attr.type === "number" ? "Число" : attr.type }),
122
- /* @__PURE__ */ jsx(
123
- "button",
124
- {
125
- onClick: () => setConfirmDeleteId(attr.id),
126
- className: "text-xs text-ui-fg-error hover:underline",
127
- children: "Удалить"
128
- }
129
- )
92
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
93
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
94
+ typeLabel(attr.type),
95
+ attr.unit ? `, ${attr.unit}` : ""
96
+ ] }),
97
+ /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
130
98
  ]
131
99
  },
132
100
  attr.id
133
- )
134
- ) }),
101
+ )) })
102
+ ] }),
103
+ ownAttributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
104
+ inheritedAttributes.length > 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsx(Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Свои" }) }),
105
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: ownAttributes.map(
106
+ (attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxs(
107
+ "div",
108
+ {
109
+ className: "flex items-center gap-3 px-6 py-3 text-sm",
110
+ children: [
111
+ /* @__PURE__ */ jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
112
+ "Удалить «",
113
+ attr.label,
114
+ "»?"
115
+ ] }),
116
+ /* @__PURE__ */ jsx(
117
+ Button,
118
+ {
119
+ size: "small",
120
+ variant: "danger",
121
+ onClick: () => deleteMutation.mutate(attr.id),
122
+ isLoading: deleteMutation.isPending,
123
+ children: "Удалить"
124
+ }
125
+ ),
126
+ /* @__PURE__ */ jsx(
127
+ Button,
128
+ {
129
+ size: "small",
130
+ variant: "secondary",
131
+ onClick: () => setConfirmDeleteId(null),
132
+ children: "Отмена"
133
+ }
134
+ )
135
+ ]
136
+ },
137
+ attr.id
138
+ ) : /* @__PURE__ */ jsxs(
139
+ "div",
140
+ {
141
+ className: "flex items-center gap-3 px-6 py-3",
142
+ children: [
143
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
144
+ /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
145
+ /* @__PURE__ */ jsx(
146
+ "button",
147
+ {
148
+ onClick: () => setConfirmDeleteId(attr.id),
149
+ className: "text-xs text-ui-fg-error hover:underline",
150
+ children: "Удалить"
151
+ }
152
+ )
153
+ ]
154
+ },
155
+ attr.id
156
+ )
157
+ ) })
158
+ ] }),
159
+ !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
135
160
  showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
136
161
  /* @__PURE__ */ jsx(
137
162
  Input,
@@ -149,15 +174,27 @@ const CategoryAttributeTemplatesWidget = ({
149
174
  value: addForm.type,
150
175
  onChange: (e) => setAddForm((f) => ({
151
176
  ...f,
152
- type: e.target.value
177
+ type: e.target.value,
178
+ unit: e.target.value === "number" ? f.unit : ""
153
179
  })),
154
180
  className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
155
181
  children: [
156
182
  /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
157
- /* @__PURE__ */ jsx("option", { value: "number", children: "Число" })
183
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
184
+ /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
185
+ /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
158
186
  ]
159
187
  }
160
188
  ),
189
+ addForm.type === "number" && /* @__PURE__ */ jsx(
190
+ Input,
191
+ {
192
+ value: addForm.unit,
193
+ onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
194
+ placeholder: "ед. (кг, м, шт...)",
195
+ className: "w-28 h-8 text-sm"
196
+ }
197
+ ),
161
198
  /* @__PURE__ */ jsx(
162
199
  Button,
163
200
  {
@@ -3,11 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GET = GET;
4
4
  exports.POST = POST;
5
5
  exports.PATCH = PATCH;
6
+ const utils_1 = require("@medusajs/framework/utils");
6
7
  const product_attributes_1 = require("../../../../../modules/product-attributes");
8
+ /**
9
+ * Walk up the category tree, starting from `leafId`, collecting ids of the
10
+ * category itself and all of its ancestors. Ordered leaf → root.
11
+ *
12
+ * A hard cap of 20 levels guards against cycles in malformed data.
13
+ */
14
+ async function resolveAncestorChain(productService, leafId) {
15
+ const chain = [];
16
+ let currentId = leafId;
17
+ let depth = 0;
18
+ while (currentId && depth < 20) {
19
+ chain.push(currentId);
20
+ const category = await productService.retrieveProductCategory(currentId, {
21
+ select: ["id", "parent_category_id"],
22
+ });
23
+ currentId = category?.parent_category_id ?? null;
24
+ depth++;
25
+ }
26
+ return chain;
27
+ }
7
28
  async function GET(req, res) {
8
29
  const { categoryId } = req.params;
9
30
  const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
10
- const category_custom_attributes = await service.getCategoryAttributes(categoryId);
31
+ const productService = req.scope.resolve(utils_1.Modules.PRODUCT);
32
+ const categoryIds = await resolveAncestorChain(productService, categoryId);
33
+ const category_custom_attributes = await service.getAttributesByCategoryIds(categoryIds);
11
34
  res.json({ category_custom_attributes });
12
35
  }
13
36
  async function POST(req, res) {
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../../../../../src/api/admin/category/[categoryId]/custom-attributes/route.ts"],"names":[],"mappings":";;AAIA,kBAOC;AAED,oBAoBC;AAED,sBAcC;AAhDD,kFAAmF;AAG5E,KAAK,UAAU,GAAG,CAAC,GAAkB,EAAE,GAAmB;IAC/D,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;IACjC,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAElF,MAAM,0BAA0B,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAA;IAElF,GAAG,CAAC,IAAI,CAAC,EAAE,0BAA0B,EAAE,CAAC,CAAA;AAC1C,CAAC;AAEM,KAAK,UAAU,IAAI,CAAC,GAAkB,EAAE,GAAmB;IAChE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;IACjC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,IAK7C,CAAA;IAED,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAElF,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,uBAAuB,CAAC;QACtE,KAAK;QACL,IAAI,EAAE,IAAI,IAAI,MAAM;QACpB,IAAI,EAAE,IAAI,IAAI,IAAI;QAClB,WAAW,EAAE,UAAU;QACvB,UAAU;KACX,CAAC,CAAA;IAEF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,yBAAyB,EAAE,CAAC,CAAA;AACrD,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;IAED,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,uBAAuB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAEjF,GAAG,CAAC,IAAI,CAAC,EAAE,yBAAyB,EAAE,CAAC,CAAA;AACzC,CAAC"}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../../../../../../src/api/admin/category/[categoryId]/custom-attributes/route.ts"],"names":[],"mappings":";;AA6BA,kBASC;AAED,oBAoBC;AAED,sBAcC;AA3ED,qDAAmD;AACnD,kFAAmF;AAGnF;;;;;GAKG;AACH,KAAK,UAAU,oBAAoB,CACjC,cAAmB,EACnB,MAAc;IAEd,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,SAAS,GAAkB,MAAM,CAAA;IACrC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,OAAO,SAAS,IAAI,KAAK,GAAG,EAAE,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACrB,MAAM,QAAQ,GAAQ,MAAM,cAAc,CAAC,uBAAuB,CAAC,SAAS,EAAE;YAC5E,MAAM,EAAE,CAAC,IAAI,EAAE,oBAAoB,CAAC;SACrC,CAAC,CAAA;QACF,SAAS,GAAI,QAAQ,EAAE,kBAAoC,IAAI,IAAI,CAAA;QACnE,KAAK,EAAE,CAAA;IACT,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAEM,KAAK,UAAU,GAAG,CAAC,GAAkB,EAAE,GAAmB;IAC/D,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;IACjC,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAClF,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,eAAO,CAAC,OAAO,CAAC,CAAA;IAEzD,MAAM,WAAW,GAAG,MAAM,oBAAoB,CAAC,cAAc,EAAE,UAAU,CAAC,CAAA;IAC1E,MAAM,0BAA0B,GAAG,MAAM,OAAO,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAA;IAExF,GAAG,CAAC,IAAI,CAAC,EAAE,0BAA0B,EAAE,CAAC,CAAA;AAC1C,CAAC;AAEM,KAAK,UAAU,IAAI,CAAC,GAAkB,EAAE,GAAmB;IAChE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;IACjC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,IAK7C,CAAA;IAED,MAAM,OAAO,GAA2B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,4CAAuB,CAAC,CAAA;IAElF,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,uBAAuB,CAAC;QACtE,KAAK;QACL,IAAI,EAAE,IAAI,IAAI,MAAM;QACpB,IAAI,EAAE,IAAI,IAAI,IAAI;QAClB,WAAW,EAAE,UAAU;QACvB,UAAU;KACX,CAAC,CAAA;IAEF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,yBAAyB,EAAE,CAAC,CAAA;AACrD,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;IAED,MAAM,yBAAyB,GAAG,MAAM,OAAO,CAAC,uBAAuB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAEjF,GAAG,CAAC,IAAI,CAAC,EAAE,yBAAyB,EAAE,CAAC,CAAA;AACzC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ export declare class Migration20260405120000 extends Migration {
3
+ up(): Promise<void>;
4
+ down(): Promise<void>;
5
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Migration20260405120000 = void 0;
4
+ const migrations_1 = require("@mikro-orm/migrations");
5
+ class Migration20260405120000 extends migrations_1.Migration {
6
+ async up() {
7
+ this.addSql(`
8
+ CREATE TABLE IF NOT EXISTS "category_custom_attribute" (
9
+ "id" text NOT NULL,
10
+ "key" text NOT NULL,
11
+ "type" text NOT NULL DEFAULT 'text',
12
+ "label" text NOT NULL DEFAULT '',
13
+ "unit" text NULL,
14
+ "sort_order" integer NOT NULL DEFAULT 0,
15
+ "category_id" text NOT NULL,
16
+ "is_standard" boolean NOT NULL DEFAULT false,
17
+ "created_at" timestamptz NOT NULL DEFAULT now(),
18
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
19
+ "deleted_at" timestamptz NULL,
20
+ CONSTRAINT "category_custom_attribute_pkey" PRIMARY KEY ("id")
21
+ );
22
+ `);
23
+ this.addSql(`
24
+ CREATE INDEX IF NOT EXISTS "IDX_category_custom_attribute_category_id"
25
+ ON "category_custom_attribute" ("category_id")
26
+ WHERE "deleted_at" IS NULL;
27
+ `);
28
+ this.addSql(`
29
+ CREATE INDEX IF NOT EXISTS "IDX_category_custom_attribute_deleted_at"
30
+ ON "category_custom_attribute" ("deleted_at")
31
+ WHERE "deleted_at" IS NOT NULL;
32
+ `);
33
+ this.addSql(`
34
+ CREATE TABLE IF NOT EXISTS "product_custom_attribute" (
35
+ "id" text NOT NULL,
36
+ "product_id" text NOT NULL,
37
+ "value" text NOT NULL,
38
+ "value_numeric" integer NULL,
39
+ "value_file" text NULL,
40
+ "category_custom_attribute_id" text NOT NULL,
41
+ "created_at" timestamptz NOT NULL DEFAULT now(),
42
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
43
+ "deleted_at" timestamptz NULL,
44
+ CONSTRAINT "product_custom_attribute_pkey" PRIMARY KEY ("id"),
45
+ CONSTRAINT "product_custom_attribute_category_custom_attribute_id_foreign"
46
+ FOREIGN KEY ("category_custom_attribute_id")
47
+ REFERENCES "category_custom_attribute" ("id")
48
+ ON UPDATE CASCADE ON DELETE CASCADE
49
+ );
50
+ `);
51
+ this.addSql(`
52
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_product_id"
53
+ ON "product_custom_attribute" ("product_id")
54
+ WHERE "deleted_at" IS NULL;
55
+ `);
56
+ this.addSql(`
57
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_category_custom_attribute_id"
58
+ ON "product_custom_attribute" ("category_custom_attribute_id")
59
+ WHERE "deleted_at" IS NULL;
60
+ `);
61
+ this.addSql(`
62
+ CREATE INDEX IF NOT EXISTS "IDX_product_custom_attribute_deleted_at"
63
+ ON "product_custom_attribute" ("deleted_at")
64
+ WHERE "deleted_at" IS NOT NULL;
65
+ `);
66
+ }
67
+ async down() {
68
+ this.addSql(`DROP TABLE IF EXISTS "product_custom_attribute" CASCADE;`);
69
+ this.addSql(`DROP TABLE IF EXISTS "category_custom_attribute" CASCADE;`);
70
+ }
71
+ }
72
+ exports.Migration20260405120000 = Migration20260405120000;
73
+ //# sourceMappingURL=Migration20260405120000.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Migration20260405120000.js","sourceRoot":"","sources":["../../../../../../src/modules/product-attributes/migrations/Migration20260405120000.ts"],"names":[],"mappings":";;;AAAA,sDAAiD;AAEjD,MAAa,uBAAwB,SAAQ,sBAAS;IAC3C,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;KAiBX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAA;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,0DAA0D,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,2DAA2D,CAAC,CAAA;IAC1E,CAAC;CACF;AAzED,0DAyEC"}
@@ -63,6 +63,7 @@ declare class CustomAttributeService extends CustomAttributeService_base {
63
63
  updated_at: Date;
64
64
  deleted_at: Date | null;
65
65
  }[]>;
66
+ getAttributesByCategoryIds(categoryIds: string[]): Promise<any[]>;
66
67
  createCategoryAttribute(data: {
67
68
  label: string;
68
69
  type: string;
@@ -19,6 +19,24 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
19
19
  deleted_at: null,
20
20
  });
21
21
  }
22
+ async getAttributesByCategoryIds(categoryIds) {
23
+ if (!categoryIds || categoryIds.length === 0) {
24
+ return [];
25
+ }
26
+ // @ts-ignore - method generated by MedusaService
27
+ const attributes = await this.listCategoryCustomAttributes({
28
+ category_id: categoryIds,
29
+ deleted_at: null,
30
+ });
31
+ // First id in the list is the leaf category — its attributes are "own",
32
+ // everything else is inherited from an ancestor.
33
+ const leafId = categoryIds[0];
34
+ return attributes.map((attr) => ({
35
+ ...attr,
36
+ inherited: attr.category_id !== leafId,
37
+ source_category_id: attr.category_id,
38
+ }));
39
+ }
22
40
  async createCategoryAttribute(data) {
23
41
  const key = this.generateKey(data.label);
24
42
  // @ts-ignore - method generated by MedusaService
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../../src/modules/product-attributes/service.ts"],"names":[],"mappings":";;;;;AAAA,qDAAyD;AACzD,mGAAwE;AACxE,iGAAsE;AAEtE,MAAM,MAAM,GAAG;IACb,uBAAuB,EAAvB,mCAAuB;IACvB,sBAAsB,EAAtB,kCAAsB;CACvB,CAAA;AAID,2EAA2E;AAC3E,MAAM,sBAAuB,SAAQ,IAAA,qBAAa,EAAC,MAAM,CAAC;IACxD,KAAK,CAAC,qBAAqB,CAAC,UAAkB;QAC5C,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,4BAA4B,CAAC;YAC7C,WAAW,EAAE,UAAU;YACvB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,IAO7B;QACC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACxC,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,8BAA8B,CAAC;YAC/C,GAAG,IAAI;YACP,GAAG;YACH,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,KAAK;YACtC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC;SACjC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAC3B,EAAU,EACV,IAMC;QAED,MAAM,UAAU,GAAwB,EAAE,GAAG,IAAI,EAAE,CAAA;QACnD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,UAAU,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,CAAC;QACD,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,8BAA8B,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;IAC3E,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,IAM5B;QACC,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,sBAAsB,CAC1B,EAAU,EACV,IAKC;QAED,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,6BAA6B,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;IACpE,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,SAAiB;QAC1C,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,2BAA2B,CAAC;YAC5C,UAAU,EAAE,SAAS;YACrB,UAAU,EAAE,IAAI;SACjB,EAAE;YACD,SAAS,EAAE,CAAC,2BAA2B,CAAC;SACzC,CAAC,CAAA;IACJ,CAAC;IAEO,WAAW,CAAC,KAAa;QAC/B,OAAO,KAAK;aACT,WAAW,EAAE;aACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;aAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC1B,CAAC;CACF;AAED,kBAAe,sBAAsB,CAAA"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../../src/modules/product-attributes/service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empty-complete-org/medusa-product-attributes",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Custom attributes module for Medusa v2 with support for text, number, file types and units of measurement",
5
5
  "author": "empty-complete",
6
6
  "license": "MIT",
@@ -63,6 +63,8 @@
63
63
  "@medusajs/js-sdk": "^2.13.1",
64
64
  "@medusajs/medusa": "^2.13.1",
65
65
  "@medusajs/ui": "^4.0.27",
66
+ "@mikro-orm/core": "6.4.16",
67
+ "@mikro-orm/migrations": "6.4.16",
66
68
  "@tanstack/react-query": "^5.64.2",
67
69
  "@types/jest": "^30.0.0",
68
70
  "@types/node": "^20.0.0",
@@ -71,6 +73,7 @@
71
73
  "jest": "^30.3.0",
72
74
  "react": "^19.0.0",
73
75
  "ts-jest": "^29.4.6",
76
+ "ts-node": "^10.9.2",
74
77
  "typescript": "^5.0.0",
75
78
  "yalc": "^1.0.0-pre.53"
76
79
  },