@empty-complete-org/medusa-product-attributes 0.11.1 → 0.12.1
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 +115 -13
- package/.medusa/server/src/admin/index.mjs +116 -14
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260405120000.d.ts +5 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260405120000.js +73 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260405120000.js.map +1 -0
- package/package.json +5 -2
|
@@ -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
|
}) => {
|
|
@@ -65,10 +65,11 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
65
65
|
if (!addForm.label.trim()) return;
|
|
66
66
|
createMutation.mutate({
|
|
67
67
|
label: addForm.label.trim(),
|
|
68
|
-
type: addForm.type
|
|
68
|
+
type: addForm.type,
|
|
69
|
+
unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
|
|
69
70
|
});
|
|
70
71
|
};
|
|
71
|
-
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t;
|
|
72
|
+
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
|
|
72
73
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
73
74
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
74
75
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
|
|
@@ -92,7 +93,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
92
93
|
className: "flex items-center gap-3 px-6 py-3",
|
|
93
94
|
children: [
|
|
94
95
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
|
|
95
|
-
/* @__PURE__ */ jsxRuntime.
|
|
96
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
|
|
97
|
+
typeLabel(attr.type),
|
|
98
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
99
|
+
] }),
|
|
96
100
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
|
|
97
101
|
]
|
|
98
102
|
},
|
|
@@ -140,7 +144,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
140
144
|
className: "flex items-center gap-3 px-6 py-3",
|
|
141
145
|
children: [
|
|
142
146
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
143
|
-
/* @__PURE__ */ jsxRuntime.
|
|
147
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
|
|
148
|
+
typeLabel(attr.type),
|
|
149
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
150
|
+
] }),
|
|
144
151
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
145
152
|
"button",
|
|
146
153
|
{
|
|
@@ -173,15 +180,27 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
173
180
|
value: addForm.type,
|
|
174
181
|
onChange: (e) => setAddForm((f) => ({
|
|
175
182
|
...f,
|
|
176
|
-
type: e.target.value
|
|
183
|
+
type: e.target.value,
|
|
184
|
+
unit: e.target.value === "number" ? f.unit : ""
|
|
177
185
|
})),
|
|
178
186
|
className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
179
187
|
children: [
|
|
180
188
|
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
|
|
181
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" })
|
|
189
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
|
|
190
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
|
|
191
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
|
|
182
192
|
]
|
|
183
193
|
}
|
|
184
194
|
),
|
|
195
|
+
addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
196
|
+
ui.Input,
|
|
197
|
+
{
|
|
198
|
+
value: addForm.unit,
|
|
199
|
+
onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
|
|
200
|
+
placeholder: "ед. (кг, м, шт...)",
|
|
201
|
+
className: "w-28 h-8 text-sm"
|
|
202
|
+
}
|
|
203
|
+
),
|
|
185
204
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
186
205
|
ui.Button,
|
|
187
206
|
{
|
|
@@ -220,6 +239,8 @@ const ProductAttributeValuesWidget = ({
|
|
|
220
239
|
const qc = reactQuery.useQueryClient();
|
|
221
240
|
const [formValues, setFormValues] = react.useState({});
|
|
222
241
|
const [saveSuccess, setSaveSuccess] = react.useState(false);
|
|
242
|
+
const [uploadingId, setUploadingId] = react.useState(null);
|
|
243
|
+
const fileInputs = react.useRef({});
|
|
223
244
|
const attributesQuery = reactQuery.useQuery({
|
|
224
245
|
queryKey: ["category-custom-attributes", categoryId],
|
|
225
246
|
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
@@ -235,7 +256,12 @@ const ProductAttributeValuesWidget = ({
|
|
|
235
256
|
const values = valuesQuery.data.product_custom_attributes;
|
|
236
257
|
const initial = {};
|
|
237
258
|
for (const attr of attributes2) {
|
|
238
|
-
const existing = values.find(
|
|
259
|
+
const existing = values.find(
|
|
260
|
+
(v) => {
|
|
261
|
+
var _a2;
|
|
262
|
+
return ((_a2 = v.category_custom_attribute) == null ? void 0 : _a2.id) === attr.id || v.category_custom_attribute_id === attr.id;
|
|
263
|
+
}
|
|
264
|
+
);
|
|
239
265
|
initial[attr.id] = existing ? existing.value : "";
|
|
240
266
|
}
|
|
241
267
|
setFormValues(initial);
|
|
@@ -256,11 +282,27 @@ const ProductAttributeValuesWidget = ({
|
|
|
256
282
|
const handleSave = () => {
|
|
257
283
|
if (!attributesQuery.data) return;
|
|
258
284
|
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
259
|
-
const attributesToUpdate = attributes2.filter(
|
|
260
|
-
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
261
|
-
).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
|
|
285
|
+
const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
|
|
262
286
|
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
263
287
|
};
|
|
288
|
+
const handleFileUpload = async (attrId, file) => {
|
|
289
|
+
var _a2, _b2;
|
|
290
|
+
setUploadingId(attrId);
|
|
291
|
+
try {
|
|
292
|
+
const formData = new FormData();
|
|
293
|
+
formData.append("files", file);
|
|
294
|
+
const res = await sdk.client.fetch(`/admin/uploads`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
body: formData
|
|
297
|
+
});
|
|
298
|
+
const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
|
|
299
|
+
if (url) {
|
|
300
|
+
setFormValues((prev) => ({ ...prev, [attrId]: url }));
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
setUploadingId(null);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
264
306
|
if (!categoryId) {
|
|
265
307
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
266
308
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
|
|
@@ -281,8 +323,68 @@ const ProductAttributeValuesWidget = ({
|
|
|
281
323
|
{
|
|
282
324
|
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
283
325
|
children: [
|
|
284
|
-
/* @__PURE__ */ jsxRuntime.
|
|
285
|
-
|
|
326
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
327
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
328
|
+
attr.unit && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: attr.unit })
|
|
329
|
+
] }),
|
|
330
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
331
|
+
"select",
|
|
332
|
+
{
|
|
333
|
+
value: formValues[attr.id] ?? "",
|
|
334
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
335
|
+
...prev,
|
|
336
|
+
[attr.id]: e.target.value
|
|
337
|
+
})),
|
|
338
|
+
className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
339
|
+
children: [
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "—" }),
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "Да" }),
|
|
342
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "Нет" })
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
) : attr.type === "file" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
347
|
+
"input",
|
|
348
|
+
{
|
|
349
|
+
ref: (el) => {
|
|
350
|
+
fileInputs.current[attr.id] = el;
|
|
351
|
+
},
|
|
352
|
+
type: "file",
|
|
353
|
+
className: "hidden",
|
|
354
|
+
onChange: (e) => {
|
|
355
|
+
var _a2;
|
|
356
|
+
const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
|
|
357
|
+
if (f) handleFileUpload(attr.id, f);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
),
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
362
|
+
ui.Input,
|
|
363
|
+
{
|
|
364
|
+
value: formValues[attr.id] ?? "",
|
|
365
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
366
|
+
...prev,
|
|
367
|
+
[attr.id]: e.target.value
|
|
368
|
+
})),
|
|
369
|
+
placeholder: "URL файла",
|
|
370
|
+
className: "h-8 text-sm flex-1"
|
|
371
|
+
}
|
|
372
|
+
),
|
|
373
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
374
|
+
ui.Button,
|
|
375
|
+
{
|
|
376
|
+
size: "small",
|
|
377
|
+
variant: "secondary",
|
|
378
|
+
type: "button",
|
|
379
|
+
onClick: () => {
|
|
380
|
+
var _a2;
|
|
381
|
+
return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
|
|
382
|
+
},
|
|
383
|
+
isLoading: uploadingId === attr.id,
|
|
384
|
+
children: "Загрузить"
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
286
388
|
ui.Input,
|
|
287
389
|
{
|
|
288
390
|
type: attr.type === "number" ? "number" : "text",
|
|
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
import { defineWidgetConfig } 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 { useState, useEffect } from "react";
|
|
5
|
+
import { useState, useRef, useEffect } from "react";
|
|
6
6
|
import Medusa from "@medusajs/js-sdk";
|
|
7
7
|
const sdk = new Medusa({
|
|
8
8
|
baseUrl: "/",
|
|
@@ -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
|
}) => {
|
|
@@ -62,10 +62,11 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
62
62
|
if (!addForm.label.trim()) return;
|
|
63
63
|
createMutation.mutate({
|
|
64
64
|
label: addForm.label.trim(),
|
|
65
|
-
type: addForm.type
|
|
65
|
+
type: addForm.type,
|
|
66
|
+
unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
|
|
66
67
|
});
|
|
67
68
|
};
|
|
68
|
-
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t;
|
|
69
|
+
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
|
|
69
70
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
70
71
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
71
72
|
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
|
|
@@ -89,7 +90,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
89
90
|
className: "flex items-center gap-3 px-6 py-3",
|
|
90
91
|
children: [
|
|
91
92
|
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-subtle", children: attr.label }),
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
|
|
94
|
+
typeLabel(attr.type),
|
|
95
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
96
|
+
] }),
|
|
93
97
|
/* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "blue", children: "из родителя" })
|
|
94
98
|
]
|
|
95
99
|
},
|
|
@@ -137,7 +141,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
137
141
|
className: "flex items-center gap-3 px-6 py-3",
|
|
138
142
|
children: [
|
|
139
143
|
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
140
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
|
|
145
|
+
typeLabel(attr.type),
|
|
146
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
147
|
+
] }),
|
|
141
148
|
/* @__PURE__ */ jsx(
|
|
142
149
|
"button",
|
|
143
150
|
{
|
|
@@ -170,15 +177,27 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
170
177
|
value: addForm.type,
|
|
171
178
|
onChange: (e) => setAddForm((f) => ({
|
|
172
179
|
...f,
|
|
173
|
-
type: e.target.value
|
|
180
|
+
type: e.target.value,
|
|
181
|
+
unit: e.target.value === "number" ? f.unit : ""
|
|
174
182
|
})),
|
|
175
183
|
className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
176
184
|
children: [
|
|
177
185
|
/* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
|
|
178
|
-
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" })
|
|
186
|
+
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
|
|
187
|
+
/* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
|
|
188
|
+
/* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
|
|
179
189
|
]
|
|
180
190
|
}
|
|
181
191
|
),
|
|
192
|
+
addForm.type === "number" && /* @__PURE__ */ jsx(
|
|
193
|
+
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
|
+
),
|
|
182
201
|
/* @__PURE__ */ jsx(
|
|
183
202
|
Button,
|
|
184
203
|
{
|
|
@@ -217,6 +236,8 @@ const ProductAttributeValuesWidget = ({
|
|
|
217
236
|
const qc = useQueryClient();
|
|
218
237
|
const [formValues, setFormValues] = useState({});
|
|
219
238
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
239
|
+
const [uploadingId, setUploadingId] = useState(null);
|
|
240
|
+
const fileInputs = useRef({});
|
|
220
241
|
const attributesQuery = useQuery({
|
|
221
242
|
queryKey: ["category-custom-attributes", categoryId],
|
|
222
243
|
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
@@ -232,7 +253,12 @@ const ProductAttributeValuesWidget = ({
|
|
|
232
253
|
const values = valuesQuery.data.product_custom_attributes;
|
|
233
254
|
const initial = {};
|
|
234
255
|
for (const attr of attributes2) {
|
|
235
|
-
const existing = values.find(
|
|
256
|
+
const existing = values.find(
|
|
257
|
+
(v) => {
|
|
258
|
+
var _a2;
|
|
259
|
+
return ((_a2 = v.category_custom_attribute) == null ? void 0 : _a2.id) === attr.id || v.category_custom_attribute_id === attr.id;
|
|
260
|
+
}
|
|
261
|
+
);
|
|
236
262
|
initial[attr.id] = existing ? existing.value : "";
|
|
237
263
|
}
|
|
238
264
|
setFormValues(initial);
|
|
@@ -253,11 +279,27 @@ const ProductAttributeValuesWidget = ({
|
|
|
253
279
|
const handleSave = () => {
|
|
254
280
|
if (!attributesQuery.data) return;
|
|
255
281
|
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
256
|
-
const attributesToUpdate = attributes2.filter(
|
|
257
|
-
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
258
|
-
).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
|
|
282
|
+
const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
|
|
259
283
|
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
260
284
|
};
|
|
285
|
+
const handleFileUpload = async (attrId, file) => {
|
|
286
|
+
var _a2, _b2;
|
|
287
|
+
setUploadingId(attrId);
|
|
288
|
+
try {
|
|
289
|
+
const formData = new FormData();
|
|
290
|
+
formData.append("files", file);
|
|
291
|
+
const res = await sdk.client.fetch(`/admin/uploads`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: formData
|
|
294
|
+
});
|
|
295
|
+
const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
|
|
296
|
+
if (url) {
|
|
297
|
+
setFormValues((prev) => ({ ...prev, [attrId]: url }));
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
setUploadingId(null);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
261
303
|
if (!categoryId) {
|
|
262
304
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
263
305
|
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
@@ -278,8 +320,68 @@ const ProductAttributeValuesWidget = ({
|
|
|
278
320
|
{
|
|
279
321
|
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
280
322
|
children: [
|
|
281
|
-
/* @__PURE__ */
|
|
282
|
-
|
|
323
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
324
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
325
|
+
attr.unit && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: attr.unit })
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxs(
|
|
328
|
+
"select",
|
|
329
|
+
{
|
|
330
|
+
value: formValues[attr.id] ?? "",
|
|
331
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
332
|
+
...prev,
|
|
333
|
+
[attr.id]: e.target.value
|
|
334
|
+
})),
|
|
335
|
+
className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "—" }),
|
|
338
|
+
/* @__PURE__ */ jsx("option", { value: "true", children: "Да" }),
|
|
339
|
+
/* @__PURE__ */ jsx("option", { value: "false", children: "Нет" })
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
) : attr.type === "file" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
343
|
+
/* @__PURE__ */ jsx(
|
|
344
|
+
"input",
|
|
345
|
+
{
|
|
346
|
+
ref: (el) => {
|
|
347
|
+
fileInputs.current[attr.id] = el;
|
|
348
|
+
},
|
|
349
|
+
type: "file",
|
|
350
|
+
className: "hidden",
|
|
351
|
+
onChange: (e) => {
|
|
352
|
+
var _a2;
|
|
353
|
+
const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
|
|
354
|
+
if (f) handleFileUpload(attr.id, f);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
/* @__PURE__ */ jsx(
|
|
359
|
+
Input,
|
|
360
|
+
{
|
|
361
|
+
value: formValues[attr.id] ?? "",
|
|
362
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
363
|
+
...prev,
|
|
364
|
+
[attr.id]: e.target.value
|
|
365
|
+
})),
|
|
366
|
+
placeholder: "URL файла",
|
|
367
|
+
className: "h-8 text-sm flex-1"
|
|
368
|
+
}
|
|
369
|
+
),
|
|
370
|
+
/* @__PURE__ */ jsx(
|
|
371
|
+
Button,
|
|
372
|
+
{
|
|
373
|
+
size: "small",
|
|
374
|
+
variant: "secondary",
|
|
375
|
+
type: "button",
|
|
376
|
+
onClick: () => {
|
|
377
|
+
var _a2;
|
|
378
|
+
return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
|
|
379
|
+
},
|
|
380
|
+
isLoading: uploadingId === attr.id,
|
|
381
|
+
children: "Загрузить"
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
] }) : /* @__PURE__ */ jsx(
|
|
283
385
|
Input,
|
|
284
386
|
{
|
|
285
387
|
type: attr.type === "number" ? "number" : "text",
|
|
@@ -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
|
package/.medusa/server/src/modules/product-attributes/migrations/Migration20260405120000.js.map
ADDED
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empty-complete-org/medusa-product-attributes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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
|
},
|
|
@@ -80,4 +83,4 @@
|
|
|
80
83
|
"publishConfig": {
|
|
81
84
|
"registry": "https://registry.npmjs.org"
|
|
82
85
|
}
|
|
83
|
-
}
|
|
86
|
+
}
|