@empty-complete-org/medusa-product-attributes 0.12.0 → 0.12.2
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.
|
@@ -144,7 +144,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
144
144
|
className: "flex items-center gap-3 px-6 py-3",
|
|
145
145
|
children: [
|
|
146
146
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
147
|
-
/* @__PURE__ */ jsxRuntime.
|
|
147
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
|
|
148
|
+
typeLabel(attr.type),
|
|
149
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
150
|
+
] }),
|
|
148
151
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
149
152
|
"button",
|
|
150
153
|
{
|
|
@@ -236,6 +239,8 @@ const ProductAttributeValuesWidget = ({
|
|
|
236
239
|
const qc = reactQuery.useQueryClient();
|
|
237
240
|
const [formValues, setFormValues] = react.useState({});
|
|
238
241
|
const [saveSuccess, setSaveSuccess] = react.useState(false);
|
|
242
|
+
const [uploadingId, setUploadingId] = react.useState(null);
|
|
243
|
+
const fileInputs = react.useRef({});
|
|
239
244
|
const attributesQuery = reactQuery.useQuery({
|
|
240
245
|
queryKey: ["category-custom-attributes", categoryId],
|
|
241
246
|
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
@@ -251,7 +256,12 @@ const ProductAttributeValuesWidget = ({
|
|
|
251
256
|
const values = valuesQuery.data.product_custom_attributes;
|
|
252
257
|
const initial = {};
|
|
253
258
|
for (const attr of attributes2) {
|
|
254
|
-
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
|
+
);
|
|
255
265
|
initial[attr.id] = existing ? existing.value : "";
|
|
256
266
|
}
|
|
257
267
|
setFormValues(initial);
|
|
@@ -272,11 +282,41 @@ const ProductAttributeValuesWidget = ({
|
|
|
272
282
|
const handleSave = () => {
|
|
273
283
|
if (!attributesQuery.data) return;
|
|
274
284
|
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
275
|
-
const attributesToUpdate = attributes2.filter(
|
|
276
|
-
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
277
|
-
).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] ?? "" }));
|
|
278
286
|
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
279
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 response = await fetch(`/admin/uploads`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
credentials: "include",
|
|
297
|
+
body: formData
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const text = await response.text();
|
|
301
|
+
console.error("Upload failed", response.status, text);
|
|
302
|
+
alert(`Ошибка загрузки файла: ${response.status}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const res = await response.json();
|
|
306
|
+
const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
|
|
307
|
+
if (url) {
|
|
308
|
+
setFormValues((prev) => ({ ...prev, [attrId]: url }));
|
|
309
|
+
} else {
|
|
310
|
+
console.error("No URL in upload response", res);
|
|
311
|
+
alert("Файл загружен, но URL не получен");
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error("Upload error", err);
|
|
315
|
+
alert("Ошибка загрузки файла");
|
|
316
|
+
} finally {
|
|
317
|
+
setUploadingId(null);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
280
320
|
if (!categoryId) {
|
|
281
321
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
282
322
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
|
|
@@ -297,8 +337,68 @@ const ProductAttributeValuesWidget = ({
|
|
|
297
337
|
{
|
|
298
338
|
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
299
339
|
children: [
|
|
300
|
-
/* @__PURE__ */ jsxRuntime.
|
|
301
|
-
|
|
340
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
342
|
+
attr.unit && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: attr.unit })
|
|
343
|
+
] }),
|
|
344
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
345
|
+
"select",
|
|
346
|
+
{
|
|
347
|
+
value: formValues[attr.id] ?? "",
|
|
348
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
349
|
+
...prev,
|
|
350
|
+
[attr.id]: e.target.value
|
|
351
|
+
})),
|
|
352
|
+
className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
353
|
+
children: [
|
|
354
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "—" }),
|
|
355
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "Да" }),
|
|
356
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "Нет" })
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
) : attr.type === "file" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
360
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
361
|
+
"input",
|
|
362
|
+
{
|
|
363
|
+
ref: (el) => {
|
|
364
|
+
fileInputs.current[attr.id] = el;
|
|
365
|
+
},
|
|
366
|
+
type: "file",
|
|
367
|
+
className: "hidden",
|
|
368
|
+
onChange: (e) => {
|
|
369
|
+
var _a2;
|
|
370
|
+
const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
|
|
371
|
+
if (f) handleFileUpload(attr.id, f);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
376
|
+
ui.Input,
|
|
377
|
+
{
|
|
378
|
+
value: formValues[attr.id] ?? "",
|
|
379
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
380
|
+
...prev,
|
|
381
|
+
[attr.id]: e.target.value
|
|
382
|
+
})),
|
|
383
|
+
placeholder: "URL файла",
|
|
384
|
+
className: "h-8 text-sm flex-1"
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
388
|
+
ui.Button,
|
|
389
|
+
{
|
|
390
|
+
size: "small",
|
|
391
|
+
variant: "secondary",
|
|
392
|
+
type: "button",
|
|
393
|
+
onClick: () => {
|
|
394
|
+
var _a2;
|
|
395
|
+
return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
|
|
396
|
+
},
|
|
397
|
+
isLoading: uploadingId === attr.id,
|
|
398
|
+
children: "Загрузить"
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
302
402
|
ui.Input,
|
|
303
403
|
{
|
|
304
404
|
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: "/",
|
|
@@ -141,7 +141,10 @@ const CategoryAttributeTemplatesWidget = ({
|
|
|
141
141
|
className: "flex items-center gap-3 px-6 py-3",
|
|
142
142
|
children: [
|
|
143
143
|
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
144
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
|
|
145
|
+
typeLabel(attr.type),
|
|
146
|
+
attr.unit ? `, ${attr.unit}` : ""
|
|
147
|
+
] }),
|
|
145
148
|
/* @__PURE__ */ jsx(
|
|
146
149
|
"button",
|
|
147
150
|
{
|
|
@@ -233,6 +236,8 @@ const ProductAttributeValuesWidget = ({
|
|
|
233
236
|
const qc = useQueryClient();
|
|
234
237
|
const [formValues, setFormValues] = useState({});
|
|
235
238
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
239
|
+
const [uploadingId, setUploadingId] = useState(null);
|
|
240
|
+
const fileInputs = useRef({});
|
|
236
241
|
const attributesQuery = useQuery({
|
|
237
242
|
queryKey: ["category-custom-attributes", categoryId],
|
|
238
243
|
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
@@ -248,7 +253,12 @@ const ProductAttributeValuesWidget = ({
|
|
|
248
253
|
const values = valuesQuery.data.product_custom_attributes;
|
|
249
254
|
const initial = {};
|
|
250
255
|
for (const attr of attributes2) {
|
|
251
|
-
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
|
+
);
|
|
252
262
|
initial[attr.id] = existing ? existing.value : "";
|
|
253
263
|
}
|
|
254
264
|
setFormValues(initial);
|
|
@@ -269,11 +279,41 @@ const ProductAttributeValuesWidget = ({
|
|
|
269
279
|
const handleSave = () => {
|
|
270
280
|
if (!attributesQuery.data) return;
|
|
271
281
|
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
272
|
-
const attributesToUpdate = attributes2.filter(
|
|
273
|
-
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
274
|
-
).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] ?? "" }));
|
|
275
283
|
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
276
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 response = await fetch(`/admin/uploads`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
credentials: "include",
|
|
294
|
+
body: formData
|
|
295
|
+
});
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
const text = await response.text();
|
|
298
|
+
console.error("Upload failed", response.status, text);
|
|
299
|
+
alert(`Ошибка загрузки файла: ${response.status}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const res = await response.json();
|
|
303
|
+
const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
|
|
304
|
+
if (url) {
|
|
305
|
+
setFormValues((prev) => ({ ...prev, [attrId]: url }));
|
|
306
|
+
} else {
|
|
307
|
+
console.error("No URL in upload response", res);
|
|
308
|
+
alert("Файл загружен, но URL не получен");
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error("Upload error", err);
|
|
312
|
+
alert("Ошибка загрузки файла");
|
|
313
|
+
} finally {
|
|
314
|
+
setUploadingId(null);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
277
317
|
if (!categoryId) {
|
|
278
318
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
279
319
|
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
@@ -294,8 +334,68 @@ const ProductAttributeValuesWidget = ({
|
|
|
294
334
|
{
|
|
295
335
|
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
296
336
|
children: [
|
|
297
|
-
/* @__PURE__ */
|
|
298
|
-
|
|
337
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
338
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
339
|
+
attr.unit && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: attr.unit })
|
|
340
|
+
] }),
|
|
341
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: attr.type === "boolean" ? /* @__PURE__ */ jsxs(
|
|
342
|
+
"select",
|
|
343
|
+
{
|
|
344
|
+
value: formValues[attr.id] ?? "",
|
|
345
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
346
|
+
...prev,
|
|
347
|
+
[attr.id]: e.target.value
|
|
348
|
+
})),
|
|
349
|
+
className: "h-8 flex-1 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
350
|
+
children: [
|
|
351
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "—" }),
|
|
352
|
+
/* @__PURE__ */ jsx("option", { value: "true", children: "Да" }),
|
|
353
|
+
/* @__PURE__ */ jsx("option", { value: "false", children: "Нет" })
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
) : attr.type === "file" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
357
|
+
/* @__PURE__ */ jsx(
|
|
358
|
+
"input",
|
|
359
|
+
{
|
|
360
|
+
ref: (el) => {
|
|
361
|
+
fileInputs.current[attr.id] = el;
|
|
362
|
+
},
|
|
363
|
+
type: "file",
|
|
364
|
+
className: "hidden",
|
|
365
|
+
onChange: (e) => {
|
|
366
|
+
var _a2;
|
|
367
|
+
const f = (_a2 = e.target.files) == null ? void 0 : _a2[0];
|
|
368
|
+
if (f) handleFileUpload(attr.id, f);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
),
|
|
372
|
+
/* @__PURE__ */ jsx(
|
|
373
|
+
Input,
|
|
374
|
+
{
|
|
375
|
+
value: formValues[attr.id] ?? "",
|
|
376
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
377
|
+
...prev,
|
|
378
|
+
[attr.id]: e.target.value
|
|
379
|
+
})),
|
|
380
|
+
placeholder: "URL файла",
|
|
381
|
+
className: "h-8 text-sm flex-1"
|
|
382
|
+
}
|
|
383
|
+
),
|
|
384
|
+
/* @__PURE__ */ jsx(
|
|
385
|
+
Button,
|
|
386
|
+
{
|
|
387
|
+
size: "small",
|
|
388
|
+
variant: "secondary",
|
|
389
|
+
type: "button",
|
|
390
|
+
onClick: () => {
|
|
391
|
+
var _a2;
|
|
392
|
+
return (_a2 = fileInputs.current[attr.id]) == null ? void 0 : _a2.click();
|
|
393
|
+
},
|
|
394
|
+
isLoading: uploadingId === attr.id,
|
|
395
|
+
children: "Загрузить"
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
] }) : /* @__PURE__ */ jsx(
|
|
299
399
|
Input,
|
|
300
400
|
{
|
|
301
401
|
type: attr.type === "number" ? "number" : "text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empty-complete-org/medusa-product-attributes",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|