@empty-complete-org/medusa-product-attributes 0.14.0 → 1.0.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.
Files changed (36) hide show
  1. package/.medusa/server/src/admin/index.js +453 -210
  2. package/.medusa/server/src/admin/index.mjs +454 -211
  3. package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.js +1 -1
  4. package/.medusa/server/src/api/admin/attribute-templates/[id]/apply/route.js.map +1 -0
  5. package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.js +6 -6
  6. package/.medusa/server/src/api/admin/attribute-templates/route.js.map +1 -0
  7. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +1 -1
  8. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -1
  9. package/.medusa/server/src/api/admin/global-attributes/route.d.ts +4 -0
  10. package/.medusa/server/src/api/admin/global-attributes/route.js +30 -0
  11. package/.medusa/server/src/api/admin/global-attributes/route.js.map +1 -0
  12. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.d.ts +2 -0
  13. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js +34 -0
  14. package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js.map +1 -0
  15. package/.medusa/server/src/modules/product-attributes/index.d.ts +5 -5
  16. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.d.ts +5 -0
  17. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js +58 -0
  18. package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js.map +1 -0
  19. package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.d.ts → attribute-template.d.ts} +3 -3
  20. package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.js → attribute-template.js} +3 -3
  21. package/.medusa/server/src/modules/product-attributes/models/attribute-template.js.map +1 -0
  22. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.d.ts +2 -1
  23. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js +2 -1
  24. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -1
  25. package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.d.ts +2 -1
  26. package/.medusa/server/src/modules/product-attributes/service.d.ts +56 -17
  27. package/.medusa/server/src/modules/product-attributes/service.js +44 -18
  28. package/.medusa/server/src/modules/product-attributes/service.js.map +1 -1
  29. package/README.md +96 -312
  30. package/README.ru.md +85 -0
  31. package/package.json +1 -1
  32. package/.medusa/server/src/api/admin/attribute-presets/[id]/apply/route.js.map +0 -1
  33. package/.medusa/server/src/api/admin/attribute-presets/route.js.map +0 -1
  34. package/.medusa/server/src/modules/product-attributes/models/attribute-preset.js.map +0 -1
  35. /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.d.ts +0 -0
  36. /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.d.ts +0 -0
@@ -3,7 +3,7 @@ import { defineWidgetConfig, defineRouteConfig } 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
5
  import * as React from "react";
6
- import { useState, useRef, useEffect } from "react";
6
+ import { useMemo, useState, useRef, useEffect } from "react";
7
7
  import Medusa from "@medusajs/js-sdk";
8
8
  const sdk = new Medusa({
9
9
  baseUrl: "/",
@@ -12,35 +12,201 @@ const sdk = new Medusa({
12
12
  type: "session"
13
13
  }
14
14
  });
15
- const emptyForm$1 = () => ({ label: "", type: "text", unit: "" });
15
+ const attributes$1 = "Атрибуты";
16
+ const characteristics$1 = "Характеристики";
17
+ const inherited$1 = "Унаследованные";
18
+ const own$1 = "Свои";
19
+ const global$1 = "глобальный";
20
+ const fromParent$1 = "из родителя";
21
+ const add$1 = "Добавить";
22
+ const fromTemplate$1 = "Из шаблона";
23
+ const chooseTemplate$1 = "Выберите шаблон";
24
+ const close$1 = "Закрыть";
25
+ const loading$1 = "Загрузка…";
26
+ const loadFailed$1 = "Не удалось загрузить.";
27
+ const noAttributes$1 = "Нет атрибутов. Добавьте первый.";
28
+ const noTemplates$1 = "Шаблонов нет. Создайте в Settings → Attribute Templates.";
29
+ const noGlobals$1 = "Глобальных атрибутов нет.";
30
+ const cancel$1 = "Отмена";
31
+ const confirmDelete$1 = "Удалить «{name}»?";
32
+ const create$1 = "Создать";
33
+ const save$1 = "Сохранить";
34
+ const saved$1 = "Сохранено ✓";
35
+ const name$1 = "Название";
36
+ const description$1 = "Описание (необязательно)";
37
+ const unit$1 = "ед.";
38
+ const value$1 = "Значение";
39
+ const yes$1 = "Да";
40
+ const no$1 = "Нет";
41
+ const upload$1 = "Загрузить";
42
+ const uploadError$1 = "Ошибка загрузки файла";
43
+ const fileUrl$1 = "URL файла";
44
+ const templates$1 = "Шаблоны атрибутов";
45
+ const templatesDesc$1 = "Заготовки атрибутов, которые можно применить к любой категории.";
46
+ const globals$1 = "Глобальные атрибуты";
47
+ const globalsDesc$1 = "Применяются ко всем товарам автоматически. Значения у продуктов необязательны.";
48
+ const ru = {
49
+ attributes: attributes$1,
50
+ characteristics: characteristics$1,
51
+ inherited: inherited$1,
52
+ own: own$1,
53
+ global: global$1,
54
+ fromParent: fromParent$1,
55
+ add: add$1,
56
+ fromTemplate: fromTemplate$1,
57
+ chooseTemplate: chooseTemplate$1,
58
+ close: close$1,
59
+ loading: loading$1,
60
+ loadFailed: loadFailed$1,
61
+ noAttributes: noAttributes$1,
62
+ noTemplates: noTemplates$1,
63
+ noGlobals: noGlobals$1,
64
+ "delete": "Удалить",
65
+ cancel: cancel$1,
66
+ confirmDelete: confirmDelete$1,
67
+ create: create$1,
68
+ save: save$1,
69
+ saved: saved$1,
70
+ name: name$1,
71
+ description: description$1,
72
+ unit: unit$1,
73
+ value: value$1,
74
+ "type.text": "Текст",
75
+ "type.number": "Число",
76
+ "type.file": "Файл",
77
+ "type.boolean": "Да/Нет",
78
+ yes: yes$1,
79
+ no: no$1,
80
+ upload: upload$1,
81
+ uploadError: uploadError$1,
82
+ fileUrl: fileUrl$1,
83
+ templates: templates$1,
84
+ templatesDesc: templatesDesc$1,
85
+ globals: globals$1,
86
+ globalsDesc: globalsDesc$1
87
+ };
88
+ const attributes = "Attributes";
89
+ const characteristics = "Characteristics";
90
+ const inherited = "Inherited";
91
+ const own = "Own";
92
+ const global = "global";
93
+ const fromParent = "from parent";
94
+ const add = "Add";
95
+ const fromTemplate = "From template";
96
+ const chooseTemplate = "Choose a template";
97
+ const close = "Close";
98
+ const loading = "Loading…";
99
+ const loadFailed = "Failed to load.";
100
+ const noAttributes = "No attributes. Add your first.";
101
+ const noTemplates = "No templates. Create in Settings → Attribute Templates.";
102
+ const noGlobals = "No global attributes.";
103
+ const cancel = "Cancel";
104
+ const confirmDelete = "Delete «{name}»?";
105
+ const create = "Create";
106
+ const save = "Save";
107
+ const saved = "Saved ✓";
108
+ const name = "Name";
109
+ const description = "Description (optional)";
110
+ const unit = "unit";
111
+ const value = "Value";
112
+ const yes = "Yes";
113
+ const no = "No";
114
+ const upload = "Upload";
115
+ const uploadError = "Upload failed";
116
+ const fileUrl = "File URL";
117
+ const templates = "Attribute Templates";
118
+ const templatesDesc = "Reusable attribute blueprints. Apply to any category in one click.";
119
+ const globals = "Global Attributes";
120
+ const globalsDesc = "Applied to every product automatically. Product values are optional.";
121
+ const en = {
122
+ attributes,
123
+ characteristics,
124
+ inherited,
125
+ own,
126
+ global,
127
+ fromParent,
128
+ add,
129
+ fromTemplate,
130
+ chooseTemplate,
131
+ close,
132
+ loading,
133
+ loadFailed,
134
+ noAttributes,
135
+ noTemplates,
136
+ noGlobals,
137
+ "delete": "Delete",
138
+ cancel,
139
+ confirmDelete,
140
+ create,
141
+ save,
142
+ saved,
143
+ name,
144
+ description,
145
+ unit,
146
+ value,
147
+ "type.text": "Text",
148
+ "type.number": "Number",
149
+ "type.file": "File",
150
+ "type.boolean": "Yes/No",
151
+ yes,
152
+ no,
153
+ upload,
154
+ uploadError,
155
+ fileUrl,
156
+ templates,
157
+ templatesDesc,
158
+ globals,
159
+ globalsDesc
160
+ };
161
+ const locales = { ru, en };
162
+ function detectLang() {
163
+ var _a;
164
+ if (typeof window === "undefined") return "en";
165
+ const stored = window.localStorage.getItem("i18nextLng");
166
+ if (stored) {
167
+ const short = stored.slice(0, 2).toLowerCase();
168
+ if (locales[short]) return short;
169
+ }
170
+ const nav = (((_a = window.navigator) == null ? void 0 : _a.language) || "en").slice(0, 2).toLowerCase();
171
+ return locales[nav] ? nav : "en";
172
+ }
173
+ function useT() {
174
+ const dict = useMemo(() => {
175
+ const lang = detectLang();
176
+ return locales[lang] ?? locales.en;
177
+ }, []);
178
+ return (key, fallback) => dict[key] ?? fallback ?? key;
179
+ }
180
+ const emptyForm$2 = () => ({ label: "", type: "text", unit: "" });
16
181
  const CategoryAttributeTemplatesWidget = ({
17
182
  data
18
183
  }) => {
19
184
  var _a, _b;
20
185
  const categoryId = data.id;
186
+ const t = useT();
21
187
  const qc = useQueryClient();
22
188
  const queryKey = ["category-custom-attributes", categoryId];
23
189
  const [showAddForm, setShowAddForm] = useState(false);
24
- const [showPresetList, setShowPresetList] = useState(false);
25
- const [addForm, setAddForm] = useState(emptyForm$1());
190
+ const [showTemplateList, setShowTemplateList] = useState(false);
191
+ const [addForm, setAddForm] = useState(emptyForm$2());
26
192
  const [confirmDeleteId, setConfirmDeleteId] = useState(null);
27
193
  const [mutationError, setMutationError] = useState(null);
28
- const presetsQuery = useQuery({
29
- queryKey: ["attribute-presets"],
30
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`),
31
- enabled: showPresetList
194
+ const templatesQuery = useQuery({
195
+ queryKey: ["attribute-templates"],
196
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`),
197
+ enabled: showTemplateList
32
198
  });
33
- const applyPresetMutation = useMutation({
34
- mutationFn: (presetId) => sdk.client.fetch(`/admin/attribute-presets/${presetId}/apply`, {
199
+ const applyTemplateMutation = useMutation({
200
+ mutationFn: (templateId) => sdk.client.fetch(`/admin/attribute-templates/${templateId}/apply`, {
35
201
  method: "POST",
36
202
  body: { category_id: categoryId }
37
203
  }),
38
204
  onSuccess: () => {
39
205
  qc.invalidateQueries({ queryKey });
40
- setShowPresetList(false);
206
+ setShowTemplateList(false);
41
207
  },
42
208
  onError: (err) => {
43
- setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении пресета");
209
+ setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении шаблона");
44
210
  }
45
211
  });
46
212
  const {
@@ -51,9 +217,9 @@ const CategoryAttributeTemplatesWidget = ({
51
217
  queryKey,
52
218
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
53
219
  });
54
- const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
55
- const ownAttributes = attributes.filter((a) => !a.inherited);
56
- const inheritedAttributes = attributes.filter((a) => a.inherited);
220
+ const attributes2 = (result == null ? void 0 : result.category_custom_attributes) ?? [];
221
+ const ownAttributes = attributes2.filter((a) => !a.inherited);
222
+ const inheritedAttributes = attributes2.filter((a) => a.inherited);
57
223
  const createMutation = useMutation({
58
224
  mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
59
225
  method: "POST",
@@ -62,7 +228,7 @@ const CategoryAttributeTemplatesWidget = ({
62
228
  onSuccess: () => {
63
229
  qc.invalidateQueries({ queryKey });
64
230
  setShowAddForm(false);
65
- setAddForm(emptyForm$1());
231
+ setAddForm(emptyForm$2());
66
232
  setMutationError(null);
67
233
  },
68
234
  onError: (err) => {
@@ -87,51 +253,54 @@ const CategoryAttributeTemplatesWidget = ({
87
253
  unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
88
254
  });
89
255
  };
90
- const typeLabel2 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
256
+ const typeLabel2 = (v) => t(`type.${v}`, v);
91
257
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
92
258
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
93
- /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
94
- !showAddForm && !showPresetList && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
259
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: t("attributes") }),
260
+ !showAddForm && !showTemplateList && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
95
261
  /* @__PURE__ */ jsx(
96
262
  Button,
97
263
  {
98
264
  variant: "secondary",
99
265
  size: "small",
100
- onClick: () => setShowPresetList(true),
101
- children: "Из пресета"
266
+ onClick: () => setShowTemplateList(true),
267
+ children: t("fromTemplate")
102
268
  }
103
269
  ),
104
- /* @__PURE__ */ jsx(
270
+ /* @__PURE__ */ jsxs(
105
271
  Button,
106
272
  {
107
273
  variant: "secondary",
108
274
  size: "small",
109
275
  onClick: () => setShowAddForm(true),
110
- children: "+ Добавить"
276
+ children: [
277
+ "+ ",
278
+ t("add")
279
+ ]
111
280
  }
112
281
  )
113
282
  ] })
114
283
  ] }),
115
- showPresetList && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3", children: [
284
+ showTemplateList && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3", children: [
116
285
  /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
117
- /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Выберите пресет" }),
286
+ /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Выберите шаблон" }),
118
287
  /* @__PURE__ */ jsx(
119
288
  Button,
120
289
  {
121
290
  size: "small",
122
291
  variant: "secondary",
123
- onClick: () => setShowPresetList(false),
292
+ onClick: () => setShowTemplateList(false),
124
293
  children: "Закрыть"
125
294
  }
126
295
  )
127
296
  ] }),
128
- presetsQuery.isLoading && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }),
129
- ((_a = presetsQuery.data) == null ? void 0 : _a.attribute_presets.length) === 0 && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
130
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: (_b = presetsQuery.data) == null ? void 0 : _b.attribute_presets.map((p) => /* @__PURE__ */ jsxs(
297
+ templatesQuery.isLoading && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }),
298
+ ((_a = templatesQuery.data) == null ? void 0 : _a.attribute_templates.length) === 0 && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
299
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: (_b = templatesQuery.data) == null ? void 0 : _b.attribute_templates.map((p) => /* @__PURE__ */ jsxs(
131
300
  "button",
132
301
  {
133
- onClick: () => applyPresetMutation.mutate(p.id),
134
- disabled: applyPresetMutation.isPending,
302
+ onClick: () => applyTemplateMutation.mutate(p.id),
303
+ disabled: applyTemplateMutation.isPending,
135
304
  className: "flex items-center justify-between rounded border border-ui-border-base px-3 py-2 text-left text-sm hover:bg-ui-bg-subtle disabled:opacity-50",
136
305
  children: [
137
306
  /* @__PURE__ */ jsx("span", { children: p.label }),
@@ -144,7 +313,7 @@ const CategoryAttributeTemplatesWidget = ({
144
313
  p.id
145
314
  )) })
146
315
  ] }),
147
- isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
316
+ isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }) }),
148
317
  isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
149
318
  inheritedAttributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
150
319
  /* @__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: "Унаследованные" }) }),
@@ -223,7 +392,7 @@ const CategoryAttributeTemplatesWidget = ({
223
392
  )
224
393
  ) })
225
394
  ] }),
226
- !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: "Нет атрибутов. Добавьте первый." }) }),
395
+ !isLoading && !isError && attributes2.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
227
396
  showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
228
397
  /* @__PURE__ */ jsx(
229
398
  Input,
@@ -278,7 +447,7 @@ const CategoryAttributeTemplatesWidget = ({
278
447
  size: "small",
279
448
  onClick: () => {
280
449
  setShowAddForm(false);
281
- setAddForm(emptyForm$1());
450
+ setAddForm(emptyForm$2());
282
451
  setMutationError(null);
283
452
  },
284
453
  children: "Отмена"
@@ -294,19 +463,17 @@ defineWidgetConfig({
294
463
  const ProductAttributeValuesWidget = ({
295
464
  data
296
465
  }) => {
297
- var _a, _b, _c;
466
+ var _a;
298
467
  const productId = data.id;
299
468
  const productHandle = data.handle || productId;
300
- const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
301
469
  const qc = useQueryClient();
302
470
  const [formValues, setFormValues] = useState({});
303
471
  const [saveSuccess, setSaveSuccess] = useState(false);
304
472
  const [uploadingId, setUploadingId] = useState(null);
305
473
  const fileInputs = useRef({});
306
474
  const attributesQuery = useQuery({
307
- queryKey: ["category-custom-attributes", categoryId],
308
- queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
309
- enabled: !!categoryId
475
+ queryKey: ["product-attribute-schema", productId],
476
+ queryFn: () => sdk.client.fetch(`/admin/product/${productId}/attribute-schema`)
310
477
  });
311
478
  const valuesQuery = useQuery({
312
479
  queryKey: ["product-custom-attributes", productId],
@@ -314,10 +481,10 @@ const ProductAttributeValuesWidget = ({
314
481
  });
315
482
  useEffect(() => {
316
483
  if (!attributesQuery.data || !valuesQuery.data) return;
317
- const attributes2 = attributesQuery.data.category_custom_attributes;
484
+ const attributes22 = attributesQuery.data.attributes;
318
485
  const values = valuesQuery.data.product_custom_attributes;
319
486
  const initial = {};
320
- for (const attr of attributes2) {
487
+ for (const attr of attributes22) {
321
488
  const existing = values.find(
322
489
  (v) => {
323
490
  var _a2;
@@ -343,24 +510,28 @@ const ProductAttributeValuesWidget = ({
343
510
  });
344
511
  const handleSave = () => {
345
512
  if (!attributesQuery.data) return;
346
- const attributes2 = attributesQuery.data.category_custom_attributes;
347
- const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
513
+ const attributes22 = attributesQuery.data.attributes;
514
+ const attributesToUpdate = attributes22.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
348
515
  saveMutation.mutate({ attributes: attributesToUpdate });
349
516
  };
350
517
  const handleFileUpload = async (attr, file) => {
351
- var _a2, _b2;
518
+ var _a2, _b;
352
519
  const attrId = attr.id;
353
520
  setUploadingId(attrId);
354
521
  try {
355
- const dot = file.name.lastIndexOf(".");
356
- const ext = dot > -1 ? file.name.slice(dot) : "";
357
- const renamed = new File(
358
- [file],
359
- `${productHandle}_${attr.key}${ext}`,
360
- { type: file.type }
361
- );
522
+ const isImage = file.type.startsWith("image/");
523
+ let toUpload = file;
524
+ if (isImage) {
525
+ const dot = file.name.lastIndexOf(".");
526
+ const ext = dot > -1 ? file.name.slice(dot) : "";
527
+ toUpload = new File(
528
+ [file],
529
+ `${productHandle}_${attr.key}${ext}`,
530
+ { type: file.type }
531
+ );
532
+ }
362
533
  const formData = new FormData();
363
- formData.append("files", renamed);
534
+ formData.append("files", toUpload);
364
535
  const response = await fetch(`/admin/uploads`, {
365
536
  method: "POST",
366
537
  credentials: "include",
@@ -373,7 +544,7 @@ const ProductAttributeValuesWidget = ({
373
544
  return;
374
545
  }
375
546
  const res = await response.json();
376
- const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
547
+ const url = (_b = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b.url;
377
548
  if (url) {
378
549
  setFormValues((prev) => ({ ...prev, [attrId]: url }));
379
550
  } else {
@@ -387,22 +558,16 @@ const ProductAttributeValuesWidget = ({
387
558
  setUploadingId(null);
388
559
  }
389
560
  };
390
- if (!categoryId) {
391
- return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
392
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
393
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
394
- ] });
395
- }
396
561
  const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
397
562
  const isError = attributesQuery.isError || valuesQuery.isError;
398
- const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
563
+ const attributes2 = ((_a = attributesQuery.data) == null ? void 0 : _a.attributes) ?? [];
399
564
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
400
565
  /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
401
566
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
402
567
  isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
403
- !isLoading && !isError && attributes.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
404
- !isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
405
- /* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes.map((attr) => /* @__PURE__ */ jsxs(
568
+ !isLoading && !isError && attributes2.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
569
+ !isLoading && !isError && attributes2.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
570
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes2.map((attr) => /* @__PURE__ */ jsxs(
406
571
  "div",
407
572
  {
408
573
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
@@ -500,11 +665,76 @@ const ProductAttributeValuesWidget = ({
500
665
  defineWidgetConfig({
501
666
  zone: "product.details.after"
502
667
  });
668
+ var __defProp$1 = Object.defineProperty;
669
+ var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
670
+ var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
671
+ var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
672
+ var __defNormalProp$1 = (obj, key, value2) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
673
+ var __spreadValues$1 = (a, b) => {
674
+ for (var prop in b || (b = {}))
675
+ if (__hasOwnProp$1.call(b, prop))
676
+ __defNormalProp$1(a, prop, b[prop]);
677
+ if (__getOwnPropSymbols$1)
678
+ for (var prop of __getOwnPropSymbols$1(b)) {
679
+ if (__propIsEnum$1.call(b, prop))
680
+ __defNormalProp$1(a, prop, b[prop]);
681
+ }
682
+ return a;
683
+ };
684
+ var __objRest$1 = (source, exclude) => {
685
+ var target = {};
686
+ for (var prop in source)
687
+ if (__hasOwnProp$1.call(source, prop) && exclude.indexOf(prop) < 0)
688
+ target[prop] = source[prop];
689
+ if (source != null && __getOwnPropSymbols$1)
690
+ for (var prop of __getOwnPropSymbols$1(source)) {
691
+ if (exclude.indexOf(prop) < 0 && __propIsEnum$1.call(source, prop))
692
+ target[prop] = source[prop];
693
+ }
694
+ return target;
695
+ };
696
+ const Globe = React.forwardRef(
697
+ (_a, ref) => {
698
+ var _b = _a, { color = "currentColor" } = _b, props = __objRest$1(_b, ["color"]);
699
+ return /* @__PURE__ */ React.createElement(
700
+ "svg",
701
+ __spreadValues$1({
702
+ xmlns: "http://www.w3.org/2000/svg",
703
+ width: 15,
704
+ height: 15,
705
+ viewBox: "0 0 15 15",
706
+ fill: "none",
707
+ ref
708
+ }, props),
709
+ /* @__PURE__ */ React.createElement(
710
+ "path",
711
+ {
712
+ stroke: color,
713
+ strokeLinecap: "round",
714
+ strokeLinejoin: "round",
715
+ strokeWidth: 1.5,
716
+ d: "M7.5 13.945c1.473 0 2.667-2.886 2.667-6.445S8.973 1.056 7.5 1.056 4.833 3.94 4.833 7.5s1.194 6.445 2.667 6.445M1.056 7.5h12.888"
717
+ }
718
+ ),
719
+ /* @__PURE__ */ React.createElement(
720
+ "path",
721
+ {
722
+ stroke: color,
723
+ strokeLinecap: "round",
724
+ strokeLinejoin: "round",
725
+ strokeWidth: 1.5,
726
+ d: "M7.5 13.945a6.444 6.444 0 1 0 0-12.89 6.444 6.444 0 0 0 0 12.89"
727
+ }
728
+ )
729
+ );
730
+ }
731
+ );
732
+ Globe.displayName = "Globe";
503
733
  var __defProp = Object.defineProperty;
504
734
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
505
735
  var __hasOwnProp = Object.prototype.hasOwnProperty;
506
736
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
507
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
737
+ var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
508
738
  var __spreadValues = (a, b) => {
509
739
  for (var prop in b || (b = {}))
510
740
  if (__hasOwnProp.call(b, prop))
@@ -528,7 +758,7 @@ var __objRest = (source, exclude) => {
528
758
  }
529
759
  return target;
530
760
  };
531
- const CogSixTooth = React.forwardRef(
761
+ const SquaresPlus = React.forwardRef(
532
762
  (_a, ref) => {
533
763
  var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
534
764
  return /* @__PURE__ */ React.createElement(
@@ -542,42 +772,39 @@ const CogSixTooth = React.forwardRef(
542
772
  ref
543
773
  }, props),
544
774
  /* @__PURE__ */ React.createElement(
545
- "g",
775
+ "path",
546
776
  {
547
777
  stroke: color,
548
778
  strokeLinecap: "round",
549
779
  strokeLinejoin: "round",
550
780
  strokeWidth: 1.5,
551
- clipPath: "url(#a)"
552
- },
553
- /* @__PURE__ */ React.createElement("path", { d: "M7.5 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4" }),
554
- /* @__PURE__ */ React.createElement("path", { d: "m12.989 5.97-.826-.292a5 5 0 0 0-.323-.685 5 5 0 0 0-.43-.621l.16-.86a1.43 1.43 0 0 0-.692-1.503l-.312-.18a1.43 1.43 0 0 0-1.647.152l-.663.566a5 5 0 0 0-1.513 0L6.08 1.98a1.43 1.43 0 0 0-1.647-.152l-.312.18a1.43 1.43 0 0 0-.691 1.503l.16.857c-.32.4-.574.841-.758 1.31l-.82.29a1.43 1.43 0 0 0-.956 1.35v.36c0 .608.383 1.15.955 1.35l.826.292c.09.232.194.462.323.684.128.222.275.427.43.622l-.16.86c-.111.597.166 1.2.691 1.503l.312.18a1.43 1.43 0 0 0 1.647-.152l.663-.567a5 5 0 0 0 1.512 0l.663.568a1.43 1.43 0 0 0 1.647.152l.312-.18c.526-.304.803-.906.691-1.502l-.16-.86c.32-.398.575-.84.757-1.308l.822-.29c.572-.202.956-.743.956-1.35v-.36c0-.608-.383-1.149-.956-1.35z" })
555
- ),
556
- /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("clipPath", { id: "a" }, /* @__PURE__ */ React.createElement("path", { fill: "#fff", d: "M0 0h15v15H0z" })))
781
+ d: "M5.056 1.944H2.833a.89.89 0 0 0-.889.89v2.221c0 .491.398.89.89.89h2.222c.49 0 .888-.399.888-.89V2.833a.89.89 0 0 0-.888-.889M12.167 1.944H9.944a.89.89 0 0 0-.888.89v2.221c0 .491.398.89.888.89h2.223c.49 0 .889-.399.889-.89V2.833a.89.89 0 0 0-.89-.889M5.056 9.056H2.833a.89.89 0 0 0-.889.889v2.222c0 .49.398.889.89.889h2.222c.49 0 .888-.398.888-.89V9.946a.89.89 0 0 0-.888-.89M11.056 8.611v4.445M13.278 10.833H8.833"
782
+ }
783
+ )
557
784
  );
558
785
  }
559
786
  );
560
- CogSixTooth.displayName = "CogSixTooth";
561
- const emptyForm = () => ({
787
+ SquaresPlus.displayName = "SquaresPlus";
788
+ const emptyForm$1 = () => ({
562
789
  label: "",
563
790
  type: "text",
564
791
  unit: "",
565
792
  description: ""
566
793
  });
567
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
568
- const ProductAttributesSettingsPage = () => {
794
+ const typeLabel$1 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
795
+ const AttributeTemplatesSettingsPage = () => {
569
796
  const qc = useQueryClient();
570
- const queryKey = ["attribute-presets"];
797
+ const queryKey = ["attribute-templates"];
571
798
  const [showAddForm, setShowAddForm] = useState(false);
572
- const [addForm, setAddForm] = useState(emptyForm());
799
+ const [addForm, setAddForm] = useState(emptyForm$1());
573
800
  const [confirmDeleteId, setConfirmDeleteId] = useState(null);
574
801
  const { data, isLoading, isError } = useQuery({
575
802
  queryKey,
576
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`)
803
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
577
804
  });
578
- const presets = (data == null ? void 0 : data.attribute_presets) ?? [];
805
+ const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
579
806
  const createMutation = useMutation({
580
- mutationFn: (body) => sdk.client.fetch(`/admin/attribute-presets`, {
807
+ mutationFn: (body) => sdk.client.fetch(`/admin/attribute-templates`, {
581
808
  method: "POST",
582
809
  body: {
583
810
  label: body.label,
@@ -589,11 +816,11 @@ const ProductAttributesSettingsPage = () => {
589
816
  onSuccess: () => {
590
817
  qc.invalidateQueries({ queryKey });
591
818
  setShowAddForm(false);
592
- setAddForm(emptyForm());
819
+ setAddForm(emptyForm$1());
593
820
  }
594
821
  });
595
822
  const deleteMutation = useMutation({
596
- mutationFn: (id) => sdk.client.fetch(`/admin/attribute-presets`, {
823
+ mutationFn: (id) => sdk.client.fetch(`/admin/attribute-templates`, {
597
824
  method: "PATCH",
598
825
  body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
599
826
  }),
@@ -614,154 +841,158 @@ const ProductAttributesSettingsPage = () => {
614
841
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
615
842
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
616
843
  /* @__PURE__ */ jsxs("div", { children: [
617
- /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Пресеты атрибутов" }),
618
- /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Глобальные шаблоны атрибутов, которые можно применить к любой категории." })
844
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Шаблоны атрибутов" }),
845
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Заготовки атрибутов, которые можно применить к любой категории." })
619
846
  ] }),
620
- !showAddForm && /* @__PURE__ */ jsx(
621
- Button,
622
- {
623
- variant: "secondary",
624
- size: "small",
625
- onClick: () => setShowAddForm(true),
626
- children: "+ Добавить пресет"
627
- }
628
- )
847
+ !showAddForm && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
629
848
  ] }),
630
849
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
631
- isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить пресеты." }) }),
632
- !isLoading && !isError && presets.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов пока нет. Добавьте первый." }) }),
633
- /* @__PURE__ */ jsx("div", { className: "divide-y", children: presets.map(
634
- (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs(
635
- "div",
636
- {
637
- className: "flex items-center gap-3 px-6 py-3 text-sm",
638
- children: [
639
- /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
640
- "Удалить «",
641
- p.label,
642
- "»?"
643
- ] }),
644
- /* @__PURE__ */ jsx(
645
- Button,
646
- {
647
- size: "small",
648
- variant: "danger",
649
- onClick: () => deleteMutation.mutate(p.id),
650
- isLoading: deleteMutation.isPending,
651
- children: "Удалить"
652
- }
653
- ),
654
- /* @__PURE__ */ jsx(
655
- Button,
656
- {
657
- size: "small",
658
- variant: "secondary",
659
- onClick: () => setConfirmDeleteId(null),
660
- children: "Отмена"
661
- }
662
- )
663
- ]
664
- },
665
- p.id
666
- ) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
850
+ isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить шаблоны." }) }),
851
+ !isLoading && !isError && templates2.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Шаблонов пока нет." }) }),
852
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: templates2.map(
853
+ (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
854
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
855
+ "Удалить «",
856
+ p.label,
857
+ "»?"
858
+ ] }),
859
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(p.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
860
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
861
+ ] }, p.id) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
667
862
  /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
668
863
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
669
864
  /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: p.label }),
670
865
  /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
671
- typeLabel(p.type),
866
+ typeLabel$1(p.type),
672
867
  p.unit ? `, ${p.unit}` : ""
673
868
  ] })
674
869
  ] }),
675
870
  p.description && /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
676
871
  ] }),
677
- /* @__PURE__ */ jsx(
678
- "button",
679
- {
680
- onClick: () => setConfirmDeleteId(p.id),
681
- className: "text-xs text-ui-fg-error hover:underline",
682
- children: "Удалить"
683
- }
684
- )
872
+ /* @__PURE__ */ jsx("button", { onClick: () => setConfirmDeleteId(p.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
685
873
  ] }, p.id)
686
874
  ) }),
687
875
  showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
688
876
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
689
- /* @__PURE__ */ jsx(
690
- Input,
691
- {
692
- value: addForm.label,
693
- onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
694
- placeholder: "Название (например, Сертификат)",
695
- className: "flex-1 h-8 text-sm",
696
- autoFocus: true
697
- }
698
- ),
699
- /* @__PURE__ */ jsxs(
700
- "select",
701
- {
702
- value: addForm.type,
703
- onChange: (e) => setAddForm((f) => ({
704
- ...f,
705
- type: e.target.value,
706
- unit: e.target.value === "number" ? f.unit : ""
707
- })),
708
- className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
709
- children: [
710
- /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
711
- /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
712
- /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
713
- /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
714
- ]
715
- }
716
- ),
717
- addForm.type === "number" && /* @__PURE__ */ jsx(
718
- Input,
719
- {
720
- value: addForm.unit,
721
- onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
722
- placeholder: "ед. (кг, м...)",
723
- className: "w-28 h-8 text-sm"
724
- }
725
- )
877
+ /* @__PURE__ */ jsx(Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
878
+ /* @__PURE__ */ jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
879
+ /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
880
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
881
+ /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
882
+ /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
883
+ ] }),
884
+ addForm.type === "number" && /* @__PURE__ */ jsx(Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" })
726
885
  ] }),
727
- /* @__PURE__ */ jsx(
728
- Input,
729
- {
730
- value: addForm.description,
731
- onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
732
- placeholder: "Описание (необязательно)",
733
- className: "h-8 text-sm"
734
- }
735
- ),
886
+ /* @__PURE__ */ jsx(Input, { value: addForm.description, onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })), placeholder: "Описание (необязательно)", className: "h-8 text-sm" }),
736
887
  /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
737
- /* @__PURE__ */ jsx(
738
- Button,
739
- {
740
- variant: "secondary",
741
- size: "small",
742
- onClick: () => {
743
- setShowAddForm(false);
744
- setAddForm(emptyForm());
745
- },
746
- children: "Отмена"
747
- }
748
- ),
749
- /* @__PURE__ */ jsx(
750
- Button,
751
- {
752
- size: "small",
753
- onClick: handleAdd,
754
- isLoading: createMutation.isPending,
755
- children: "Создать"
756
- }
757
- )
888
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => {
889
+ setShowAddForm(false);
890
+ setAddForm(emptyForm$1());
891
+ }, children: "Отмена" }),
892
+ /* @__PURE__ */ jsx(Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" })
758
893
  ] })
759
894
  ] })
760
895
  ] });
761
896
  };
897
+ const config$1 = defineRouteConfig({
898
+ label: "Attribute Templates",
899
+ icon: SquaresPlus
900
+ });
901
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
902
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
903
+ const GlobalAttributesSettingsPage = () => {
904
+ const qc = useQueryClient();
905
+ const queryKey = ["global-attributes"];
906
+ const [showAddForm, setShowAddForm] = useState(false);
907
+ const [addForm, setAddForm] = useState(emptyForm());
908
+ const [confirmDeleteId, setConfirmDeleteId] = useState(null);
909
+ const { data, isLoading, isError } = useQuery({
910
+ queryKey,
911
+ queryFn: () => sdk.client.fetch(`/admin/global-attributes`)
912
+ });
913
+ const attrs = (data == null ? void 0 : data.global_attributes) ?? [];
914
+ const createMutation = useMutation({
915
+ mutationFn: (body) => sdk.client.fetch(`/admin/global-attributes`, {
916
+ method: "POST",
917
+ body: {
918
+ label: body.label,
919
+ type: body.type,
920
+ unit: body.type === "number" && body.unit ? body.unit : null
921
+ }
922
+ }),
923
+ onSuccess: () => {
924
+ qc.invalidateQueries({ queryKey });
925
+ setShowAddForm(false);
926
+ setAddForm(emptyForm());
927
+ }
928
+ });
929
+ const deleteMutation = useMutation({
930
+ mutationFn: (id) => sdk.client.fetch(`/admin/global-attributes`, {
931
+ method: "PATCH",
932
+ body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
933
+ }),
934
+ onSuccess: () => {
935
+ qc.invalidateQueries({ queryKey });
936
+ setConfirmDeleteId(null);
937
+ }
938
+ });
939
+ const handleAdd = () => {
940
+ if (!addForm.label.trim()) return;
941
+ createMutation.mutate({
942
+ label: addForm.label.trim(),
943
+ type: addForm.type,
944
+ unit: addForm.unit.trim()
945
+ });
946
+ };
947
+ return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
948
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
949
+ /* @__PURE__ */ jsxs("div", { children: [
950
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Глобальные атрибуты" }),
951
+ /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Применяются ко всем товарам автоматически. Значения у продуктов необязательны." })
952
+ ] }),
953
+ !showAddForm && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
954
+ ] }),
955
+ isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
956
+ isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить." }) }),
957
+ !isLoading && !isError && attrs.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Глобальных атрибутов нет." }) }),
958
+ /* @__PURE__ */ jsx("div", { className: "divide-y", children: attrs.map(
959
+ (a) => confirmDeleteId === a.id ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
960
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
961
+ "Удалить «",
962
+ a.label,
963
+ "»?"
964
+ ] }),
965
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(a.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
966
+ /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
967
+ ] }, a.id) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
968
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm", children: a.label }),
969
+ /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
970
+ typeLabel(a.type),
971
+ a.unit ? `, ${a.unit}` : ""
972
+ ] }),
973
+ /* @__PURE__ */ jsx("button", { onClick: () => setConfirmDeleteId(a.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
974
+ ] }, a.id)
975
+ ) }),
976
+ showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-6 py-4", children: [
977
+ /* @__PURE__ */ jsx(Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
978
+ /* @__PURE__ */ jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
979
+ /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
980
+ /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
981
+ /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
982
+ /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
983
+ ] }),
984
+ addForm.type === "number" && /* @__PURE__ */ jsx(Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" }),
985
+ /* @__PURE__ */ jsx(Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" }),
986
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => {
987
+ setShowAddForm(false);
988
+ setAddForm(emptyForm());
989
+ }, children: "Отмена" })
990
+ ] })
991
+ ] });
992
+ };
762
993
  const config = defineRouteConfig({
763
- label: "Product Attributes",
764
- icon: CogSixTooth
994
+ label: "Global Attributes",
995
+ icon: Globe
765
996
  });
766
997
  const widgetModule = { widgets: [
767
998
  {
@@ -776,17 +1007,29 @@ const widgetModule = { widgets: [
776
1007
  const routeModule = {
777
1008
  routes: [
778
1009
  {
779
- Component: ProductAttributesSettingsPage,
780
- path: "/settings/product-attributes"
1010
+ Component: AttributeTemplatesSettingsPage,
1011
+ path: "/settings/attribute-templates"
1012
+ },
1013
+ {
1014
+ Component: GlobalAttributesSettingsPage,
1015
+ path: "/settings/global-attributes"
781
1016
  }
782
1017
  ]
783
1018
  };
784
1019
  const menuItemModule = {
785
1020
  menuItems: [
1021
+ {
1022
+ label: config$1.label,
1023
+ icon: config$1.icon,
1024
+ path: "/settings/attribute-templates",
1025
+ nested: void 0,
1026
+ rank: void 0,
1027
+ translationNs: void 0
1028
+ },
786
1029
  {
787
1030
  label: config.label,
788
1031
  icon: config.icon,
789
- path: "/settings/product-attributes",
1032
+ path: "/settings/global-attributes",
790
1033
  nested: void 0,
791
1034
  rank: void 0,
792
1035
  translationNs: void 0