@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.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
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((v) => v.id === attr.id);
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.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
301
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(
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__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: typeLabel(attr.type) }),
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((v) => v.id === attr.id);
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__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
298
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
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.0",
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",