@empty-complete-org/medusa-product-attributes 0.12.0 → 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.
@@ -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,27 @@ 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 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
+ };
280
306
  if (!categoryId) {
281
307
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
282
308
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
@@ -297,8 +323,68 @@ const ProductAttributeValuesWidget = ({
297
323
  {
298
324
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
299
325
  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(
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(
302
388
  ui.Input,
303
389
  {
304
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: "/",
@@ -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,27 @@ 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 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
+ };
277
303
  if (!categoryId) {
278
304
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
279
305
  /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
@@ -294,8 +320,68 @@ const ProductAttributeValuesWidget = ({
294
320
  {
295
321
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
296
322
  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(
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(
299
385
  Input,
300
386
  {
301
387
  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.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",
@@ -83,4 +83,4 @@
83
83
  "publishConfig": {
84
84
  "registry": "https://registry.npmjs.org"
85
85
  }
86
- }
86
+ }