@empty-complete-org/medusa-product-attributes 0.14.1 → 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 +441 -202
  2. package/.medusa/server/src/admin/index.mjs +442 -203
  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,12 +510,12 @@ 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 {
@@ -377,7 +544,7 @@ const ProductAttributeValuesWidget = ({
377
544
  return;
378
545
  }
379
546
  const res = await response.json();
380
- 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;
381
548
  if (url) {
382
549
  setFormValues((prev) => ({ ...prev, [attrId]: url }));
383
550
  } else {
@@ -391,22 +558,16 @@ const ProductAttributeValuesWidget = ({
391
558
  setUploadingId(null);
392
559
  }
393
560
  };
394
- if (!categoryId) {
395
- return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
396
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
397
- /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
398
- ] });
399
- }
400
561
  const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
401
562
  const isError = attributesQuery.isError || valuesQuery.isError;
402
- const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
563
+ const attributes2 = ((_a = attributesQuery.data) == null ? void 0 : _a.attributes) ?? [];
403
564
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
404
565
  /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
405
566
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
406
567
  isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
407
- !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: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
408
- !isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
409
- /* @__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(
410
571
  "div",
411
572
  {
412
573
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
@@ -504,11 +665,76 @@ const ProductAttributeValuesWidget = ({
504
665
  defineWidgetConfig({
505
666
  zone: "product.details.after"
506
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";
507
733
  var __defProp = Object.defineProperty;
508
734
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
509
735
  var __hasOwnProp = Object.prototype.hasOwnProperty;
510
736
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
511
- 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;
512
738
  var __spreadValues = (a, b) => {
513
739
  for (var prop in b || (b = {}))
514
740
  if (__hasOwnProp.call(b, prop))
@@ -532,7 +758,7 @@ var __objRest = (source, exclude) => {
532
758
  }
533
759
  return target;
534
760
  };
535
- const CogSixTooth = React.forwardRef(
761
+ const SquaresPlus = React.forwardRef(
536
762
  (_a, ref) => {
537
763
  var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
538
764
  return /* @__PURE__ */ React.createElement(
@@ -546,42 +772,39 @@ const CogSixTooth = React.forwardRef(
546
772
  ref
547
773
  }, props),
548
774
  /* @__PURE__ */ React.createElement(
549
- "g",
775
+ "path",
550
776
  {
551
777
  stroke: color,
552
778
  strokeLinecap: "round",
553
779
  strokeLinejoin: "round",
554
780
  strokeWidth: 1.5,
555
- clipPath: "url(#a)"
556
- },
557
- /* @__PURE__ */ React.createElement("path", { d: "M7.5 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4" }),
558
- /* @__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" })
559
- ),
560
- /* @__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
+ )
561
784
  );
562
785
  }
563
786
  );
564
- CogSixTooth.displayName = "CogSixTooth";
565
- const emptyForm = () => ({
787
+ SquaresPlus.displayName = "SquaresPlus";
788
+ const emptyForm$1 = () => ({
566
789
  label: "",
567
790
  type: "text",
568
791
  unit: "",
569
792
  description: ""
570
793
  });
571
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
572
- const ProductAttributesSettingsPage = () => {
794
+ const typeLabel$1 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
795
+ const AttributeTemplatesSettingsPage = () => {
573
796
  const qc = useQueryClient();
574
- const queryKey = ["attribute-presets"];
797
+ const queryKey = ["attribute-templates"];
575
798
  const [showAddForm, setShowAddForm] = useState(false);
576
- const [addForm, setAddForm] = useState(emptyForm());
799
+ const [addForm, setAddForm] = useState(emptyForm$1());
577
800
  const [confirmDeleteId, setConfirmDeleteId] = useState(null);
578
801
  const { data, isLoading, isError } = useQuery({
579
802
  queryKey,
580
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`)
803
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
581
804
  });
582
- const presets = (data == null ? void 0 : data.attribute_presets) ?? [];
805
+ const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
583
806
  const createMutation = useMutation({
584
- mutationFn: (body) => sdk.client.fetch(`/admin/attribute-presets`, {
807
+ mutationFn: (body) => sdk.client.fetch(`/admin/attribute-templates`, {
585
808
  method: "POST",
586
809
  body: {
587
810
  label: body.label,
@@ -593,11 +816,11 @@ const ProductAttributesSettingsPage = () => {
593
816
  onSuccess: () => {
594
817
  qc.invalidateQueries({ queryKey });
595
818
  setShowAddForm(false);
596
- setAddForm(emptyForm());
819
+ setAddForm(emptyForm$1());
597
820
  }
598
821
  });
599
822
  const deleteMutation = useMutation({
600
- mutationFn: (id) => sdk.client.fetch(`/admin/attribute-presets`, {
823
+ mutationFn: (id) => sdk.client.fetch(`/admin/attribute-templates`, {
601
824
  method: "PATCH",
602
825
  body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
603
826
  }),
@@ -618,154 +841,158 @@ const ProductAttributesSettingsPage = () => {
618
841
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
619
842
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
620
843
  /* @__PURE__ */ jsxs("div", { children: [
621
- /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Пресеты атрибутов" }),
622
- /* @__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: "Заготовки атрибутов, которые можно применить к любой категории." })
623
846
  ] }),
624
- !showAddForm && /* @__PURE__ */ jsx(
625
- Button,
626
- {
627
- variant: "secondary",
628
- size: "small",
629
- onClick: () => setShowAddForm(true),
630
- children: "+ Добавить пресет"
631
- }
632
- )
847
+ !showAddForm && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
633
848
  ] }),
634
849
  isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
635
- isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить пресеты." }) }),
636
- !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: "Пресетов пока нет. Добавьте первый." }) }),
637
- /* @__PURE__ */ jsx("div", { className: "divide-y", children: presets.map(
638
- (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs(
639
- "div",
640
- {
641
- className: "flex items-center gap-3 px-6 py-3 text-sm",
642
- children: [
643
- /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
644
- "Удалить «",
645
- p.label,
646
- "»?"
647
- ] }),
648
- /* @__PURE__ */ jsx(
649
- Button,
650
- {
651
- size: "small",
652
- variant: "danger",
653
- onClick: () => deleteMutation.mutate(p.id),
654
- isLoading: deleteMutation.isPending,
655
- children: "Удалить"
656
- }
657
- ),
658
- /* @__PURE__ */ jsx(
659
- Button,
660
- {
661
- size: "small",
662
- variant: "secondary",
663
- onClick: () => setConfirmDeleteId(null),
664
- children: "Отмена"
665
- }
666
- )
667
- ]
668
- },
669
- p.id
670
- ) : /* @__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: [
671
862
  /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
672
863
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
673
864
  /* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: p.label }),
674
865
  /* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
675
- typeLabel(p.type),
866
+ typeLabel$1(p.type),
676
867
  p.unit ? `, ${p.unit}` : ""
677
868
  ] })
678
869
  ] }),
679
870
  p.description && /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
680
871
  ] }),
681
- /* @__PURE__ */ jsx(
682
- "button",
683
- {
684
- onClick: () => setConfirmDeleteId(p.id),
685
- className: "text-xs text-ui-fg-error hover:underline",
686
- children: "Удалить"
687
- }
688
- )
872
+ /* @__PURE__ */ jsx("button", { onClick: () => setConfirmDeleteId(p.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
689
873
  ] }, p.id)
690
874
  ) }),
691
875
  showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
692
876
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
693
- /* @__PURE__ */ jsx(
694
- Input,
695
- {
696
- value: addForm.label,
697
- onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
698
- placeholder: "Название (например, Сертификат)",
699
- className: "flex-1 h-8 text-sm",
700
- autoFocus: true
701
- }
702
- ),
703
- /* @__PURE__ */ jsxs(
704
- "select",
705
- {
706
- value: addForm.type,
707
- onChange: (e) => setAddForm((f) => ({
708
- ...f,
709
- type: e.target.value,
710
- unit: e.target.value === "number" ? f.unit : ""
711
- })),
712
- className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
713
- children: [
714
- /* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
715
- /* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
716
- /* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
717
- /* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
718
- ]
719
- }
720
- ),
721
- addForm.type === "number" && /* @__PURE__ */ jsx(
722
- Input,
723
- {
724
- value: addForm.unit,
725
- onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
726
- placeholder: "ед. (кг, м...)",
727
- className: "w-28 h-8 text-sm"
728
- }
729
- )
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" })
730
885
  ] }),
731
- /* @__PURE__ */ jsx(
732
- Input,
733
- {
734
- value: addForm.description,
735
- onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
736
- placeholder: "Описание (необязательно)",
737
- className: "h-8 text-sm"
738
- }
739
- ),
886
+ /* @__PURE__ */ jsx(Input, { value: addForm.description, onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })), placeholder: "Описание (необязательно)", className: "h-8 text-sm" }),
740
887
  /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
741
- /* @__PURE__ */ jsx(
742
- Button,
743
- {
744
- variant: "secondary",
745
- size: "small",
746
- onClick: () => {
747
- setShowAddForm(false);
748
- setAddForm(emptyForm());
749
- },
750
- children: "Отмена"
751
- }
752
- ),
753
- /* @__PURE__ */ jsx(
754
- Button,
755
- {
756
- size: "small",
757
- onClick: handleAdd,
758
- isLoading: createMutation.isPending,
759
- children: "Создать"
760
- }
761
- )
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: "Создать" })
762
893
  ] })
763
894
  ] })
764
895
  ] });
765
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
+ };
766
993
  const config = defineRouteConfig({
767
- label: "Product Attributes",
768
- icon: CogSixTooth
994
+ label: "Global Attributes",
995
+ icon: Globe
769
996
  });
770
997
  const widgetModule = { widgets: [
771
998
  {
@@ -780,17 +1007,29 @@ const widgetModule = { widgets: [
780
1007
  const routeModule = {
781
1008
  routes: [
782
1009
  {
783
- Component: ProductAttributesSettingsPage,
784
- path: "/settings/product-attributes"
1010
+ Component: AttributeTemplatesSettingsPage,
1011
+ path: "/settings/attribute-templates"
1012
+ },
1013
+ {
1014
+ Component: GlobalAttributesSettingsPage,
1015
+ path: "/settings/global-attributes"
785
1016
  }
786
1017
  ]
787
1018
  };
788
1019
  const menuItemModule = {
789
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
+ },
790
1029
  {
791
1030
  label: config.label,
792
1031
  icon: config.icon,
793
- path: "/settings/product-attributes",
1032
+ path: "/settings/global-attributes",
794
1033
  nested: void 0,
795
1034
  rank: void 0,
796
1035
  translationNs: void 0