@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
@@ -32,35 +32,201 @@ const sdk = new Medusa__default.default({
32
32
  type: "session"
33
33
  }
34
34
  });
35
- const emptyForm$1 = () => ({ label: "", type: "text", unit: "" });
35
+ const attributes$1 = "Атрибуты";
36
+ const characteristics$1 = "Характеристики";
37
+ const inherited$1 = "Унаследованные";
38
+ const own$1 = "Свои";
39
+ const global$1 = "глобальный";
40
+ const fromParent$1 = "из родителя";
41
+ const add$1 = "Добавить";
42
+ const fromTemplate$1 = "Из шаблона";
43
+ const chooseTemplate$1 = "Выберите шаблон";
44
+ const close$1 = "Закрыть";
45
+ const loading$1 = "Загрузка…";
46
+ const loadFailed$1 = "Не удалось загрузить.";
47
+ const noAttributes$1 = "Нет атрибутов. Добавьте первый.";
48
+ const noTemplates$1 = "Шаблонов нет. Создайте в Settings → Attribute Templates.";
49
+ const noGlobals$1 = "Глобальных атрибутов нет.";
50
+ const cancel$1 = "Отмена";
51
+ const confirmDelete$1 = "Удалить «{name}»?";
52
+ const create$1 = "Создать";
53
+ const save$1 = "Сохранить";
54
+ const saved$1 = "Сохранено ✓";
55
+ const name$1 = "Название";
56
+ const description$1 = "Описание (необязательно)";
57
+ const unit$1 = "ед.";
58
+ const value$1 = "Значение";
59
+ const yes$1 = "Да";
60
+ const no$1 = "Нет";
61
+ const upload$1 = "Загрузить";
62
+ const uploadError$1 = "Ошибка загрузки файла";
63
+ const fileUrl$1 = "URL файла";
64
+ const templates$1 = "Шаблоны атрибутов";
65
+ const templatesDesc$1 = "Заготовки атрибутов, которые можно применить к любой категории.";
66
+ const globals$1 = "Глобальные атрибуты";
67
+ const globalsDesc$1 = "Применяются ко всем товарам автоматически. Значения у продуктов необязательны.";
68
+ const ru = {
69
+ attributes: attributes$1,
70
+ characteristics: characteristics$1,
71
+ inherited: inherited$1,
72
+ own: own$1,
73
+ global: global$1,
74
+ fromParent: fromParent$1,
75
+ add: add$1,
76
+ fromTemplate: fromTemplate$1,
77
+ chooseTemplate: chooseTemplate$1,
78
+ close: close$1,
79
+ loading: loading$1,
80
+ loadFailed: loadFailed$1,
81
+ noAttributes: noAttributes$1,
82
+ noTemplates: noTemplates$1,
83
+ noGlobals: noGlobals$1,
84
+ "delete": "Удалить",
85
+ cancel: cancel$1,
86
+ confirmDelete: confirmDelete$1,
87
+ create: create$1,
88
+ save: save$1,
89
+ saved: saved$1,
90
+ name: name$1,
91
+ description: description$1,
92
+ unit: unit$1,
93
+ value: value$1,
94
+ "type.text": "Текст",
95
+ "type.number": "Число",
96
+ "type.file": "Файл",
97
+ "type.boolean": "Да/Нет",
98
+ yes: yes$1,
99
+ no: no$1,
100
+ upload: upload$1,
101
+ uploadError: uploadError$1,
102
+ fileUrl: fileUrl$1,
103
+ templates: templates$1,
104
+ templatesDesc: templatesDesc$1,
105
+ globals: globals$1,
106
+ globalsDesc: globalsDesc$1
107
+ };
108
+ const attributes = "Attributes";
109
+ const characteristics = "Characteristics";
110
+ const inherited = "Inherited";
111
+ const own = "Own";
112
+ const global = "global";
113
+ const fromParent = "from parent";
114
+ const add = "Add";
115
+ const fromTemplate = "From template";
116
+ const chooseTemplate = "Choose a template";
117
+ const close = "Close";
118
+ const loading = "Loading…";
119
+ const loadFailed = "Failed to load.";
120
+ const noAttributes = "No attributes. Add your first.";
121
+ const noTemplates = "No templates. Create in Settings → Attribute Templates.";
122
+ const noGlobals = "No global attributes.";
123
+ const cancel = "Cancel";
124
+ const confirmDelete = "Delete «{name}»?";
125
+ const create = "Create";
126
+ const save = "Save";
127
+ const saved = "Saved ✓";
128
+ const name = "Name";
129
+ const description = "Description (optional)";
130
+ const unit = "unit";
131
+ const value = "Value";
132
+ const yes = "Yes";
133
+ const no = "No";
134
+ const upload = "Upload";
135
+ const uploadError = "Upload failed";
136
+ const fileUrl = "File URL";
137
+ const templates = "Attribute Templates";
138
+ const templatesDesc = "Reusable attribute blueprints. Apply to any category in one click.";
139
+ const globals = "Global Attributes";
140
+ const globalsDesc = "Applied to every product automatically. Product values are optional.";
141
+ const en = {
142
+ attributes,
143
+ characteristics,
144
+ inherited,
145
+ own,
146
+ global,
147
+ fromParent,
148
+ add,
149
+ fromTemplate,
150
+ chooseTemplate,
151
+ close,
152
+ loading,
153
+ loadFailed,
154
+ noAttributes,
155
+ noTemplates,
156
+ noGlobals,
157
+ "delete": "Delete",
158
+ cancel,
159
+ confirmDelete,
160
+ create,
161
+ save,
162
+ saved,
163
+ name,
164
+ description,
165
+ unit,
166
+ value,
167
+ "type.text": "Text",
168
+ "type.number": "Number",
169
+ "type.file": "File",
170
+ "type.boolean": "Yes/No",
171
+ yes,
172
+ no,
173
+ upload,
174
+ uploadError,
175
+ fileUrl,
176
+ templates,
177
+ templatesDesc,
178
+ globals,
179
+ globalsDesc
180
+ };
181
+ const locales = { ru, en };
182
+ function detectLang() {
183
+ var _a;
184
+ if (typeof window === "undefined") return "en";
185
+ const stored = window.localStorage.getItem("i18nextLng");
186
+ if (stored) {
187
+ const short = stored.slice(0, 2).toLowerCase();
188
+ if (locales[short]) return short;
189
+ }
190
+ const nav = (((_a = window.navigator) == null ? void 0 : _a.language) || "en").slice(0, 2).toLowerCase();
191
+ return locales[nav] ? nav : "en";
192
+ }
193
+ function useT() {
194
+ const dict = React.useMemo(() => {
195
+ const lang = detectLang();
196
+ return locales[lang] ?? locales.en;
197
+ }, []);
198
+ return (key, fallback) => dict[key] ?? fallback ?? key;
199
+ }
200
+ const emptyForm$2 = () => ({ label: "", type: "text", unit: "" });
36
201
  const CategoryAttributeTemplatesWidget = ({
37
202
  data
38
203
  }) => {
39
204
  var _a, _b;
40
205
  const categoryId = data.id;
206
+ const t = useT();
41
207
  const qc = reactQuery.useQueryClient();
42
208
  const queryKey = ["category-custom-attributes", categoryId];
43
209
  const [showAddForm, setShowAddForm] = React.useState(false);
44
- const [showPresetList, setShowPresetList] = React.useState(false);
45
- const [addForm, setAddForm] = React.useState(emptyForm$1());
210
+ const [showTemplateList, setShowTemplateList] = React.useState(false);
211
+ const [addForm, setAddForm] = React.useState(emptyForm$2());
46
212
  const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
47
213
  const [mutationError, setMutationError] = React.useState(null);
48
- const presetsQuery = reactQuery.useQuery({
49
- queryKey: ["attribute-presets"],
50
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`),
51
- enabled: showPresetList
214
+ const templatesQuery = reactQuery.useQuery({
215
+ queryKey: ["attribute-templates"],
216
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`),
217
+ enabled: showTemplateList
52
218
  });
53
- const applyPresetMutation = reactQuery.useMutation({
54
- mutationFn: (presetId) => sdk.client.fetch(`/admin/attribute-presets/${presetId}/apply`, {
219
+ const applyTemplateMutation = reactQuery.useMutation({
220
+ mutationFn: (templateId) => sdk.client.fetch(`/admin/attribute-templates/${templateId}/apply`, {
55
221
  method: "POST",
56
222
  body: { category_id: categoryId }
57
223
  }),
58
224
  onSuccess: () => {
59
225
  qc.invalidateQueries({ queryKey });
60
- setShowPresetList(false);
226
+ setShowTemplateList(false);
61
227
  },
62
228
  onError: (err) => {
63
- setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении пресета");
229
+ setMutationError((err == null ? void 0 : err.message) || "Ошибка при применении шаблона");
64
230
  }
65
231
  });
66
232
  const {
@@ -71,9 +237,9 @@ const CategoryAttributeTemplatesWidget = ({
71
237
  queryKey,
72
238
  queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
73
239
  });
74
- const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
75
- const ownAttributes = attributes.filter((a) => !a.inherited);
76
- const inheritedAttributes = attributes.filter((a) => a.inherited);
240
+ const attributes2 = (result == null ? void 0 : result.category_custom_attributes) ?? [];
241
+ const ownAttributes = attributes2.filter((a) => !a.inherited);
242
+ const inheritedAttributes = attributes2.filter((a) => a.inherited);
77
243
  const createMutation = reactQuery.useMutation({
78
244
  mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
79
245
  method: "POST",
@@ -82,7 +248,7 @@ const CategoryAttributeTemplatesWidget = ({
82
248
  onSuccess: () => {
83
249
  qc.invalidateQueries({ queryKey });
84
250
  setShowAddForm(false);
85
- setAddForm(emptyForm$1());
251
+ setAddForm(emptyForm$2());
86
252
  setMutationError(null);
87
253
  },
88
254
  onError: (err) => {
@@ -107,51 +273,54 @@ const CategoryAttributeTemplatesWidget = ({
107
273
  unit: addForm.type === "number" && addForm.unit.trim() ? addForm.unit.trim() : null
108
274
  });
109
275
  };
110
- const typeLabel2 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
276
+ const typeLabel2 = (v) => t(`type.${v}`, v);
111
277
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
112
278
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
113
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
114
- !showAddForm && !showPresetList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: t("attributes") }),
280
+ !showAddForm && !showTemplateList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
115
281
  /* @__PURE__ */ jsxRuntime.jsx(
116
282
  ui.Button,
117
283
  {
118
284
  variant: "secondary",
119
285
  size: "small",
120
- onClick: () => setShowPresetList(true),
121
- children: "Из пресета"
286
+ onClick: () => setShowTemplateList(true),
287
+ children: t("fromTemplate")
122
288
  }
123
289
  ),
124
- /* @__PURE__ */ jsxRuntime.jsx(
290
+ /* @__PURE__ */ jsxRuntime.jsxs(
125
291
  ui.Button,
126
292
  {
127
293
  variant: "secondary",
128
294
  size: "small",
129
295
  onClick: () => setShowAddForm(true),
130
- children: "+ Добавить"
296
+ children: [
297
+ "+ ",
298
+ t("add")
299
+ ]
131
300
  }
132
301
  )
133
302
  ] })
134
303
  ] }),
135
- showPresetList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3", children: [
304
+ showTemplateList && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3", children: [
136
305
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
137
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Выберите пресет" }),
306
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Выберите шаблон" }),
138
307
  /* @__PURE__ */ jsxRuntime.jsx(
139
308
  ui.Button,
140
309
  {
141
310
  size: "small",
142
311
  variant: "secondary",
143
- onClick: () => setShowPresetList(false),
312
+ onClick: () => setShowTemplateList(false),
144
313
  children: "Закрыть"
145
314
  }
146
315
  )
147
316
  ] }),
148
- presetsQuery.isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }),
149
- ((_a = presetsQuery.data) == null ? void 0 : _a.attribute_presets.length) === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
150
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: (_b = presetsQuery.data) == null ? void 0 : _b.attribute_presets.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
317
+ templatesQuery.isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }),
318
+ ((_a = templatesQuery.data) == null ? void 0 : _a.attribute_templates.length) === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов нет. Создайте в настройках Product Attributes." }),
319
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: (_b = templatesQuery.data) == null ? void 0 : _b.attribute_templates.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
151
320
  "button",
152
321
  {
153
- onClick: () => applyPresetMutation.mutate(p.id),
154
- disabled: applyPresetMutation.isPending,
322
+ onClick: () => applyTemplateMutation.mutate(p.id),
323
+ disabled: applyTemplateMutation.isPending,
155
324
  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",
156
325
  children: [
157
326
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: p.label }),
@@ -164,7 +333,7 @@ const CategoryAttributeTemplatesWidget = ({
164
333
  p.id
165
334
  )) })
166
335
  ] }),
167
- isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
336
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: t("loading") }) }),
168
337
  isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
169
338
  inheritedAttributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
170
339
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-muted uppercase", children: "Унаследованные" }) }),
@@ -243,7 +412,7 @@ const CategoryAttributeTemplatesWidget = ({
243
412
  )
244
413
  ) })
245
414
  ] }),
246
- !isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
415
+ !isLoading && !isError && attributes2.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
247
416
  showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
248
417
  /* @__PURE__ */ jsxRuntime.jsx(
249
418
  ui.Input,
@@ -298,7 +467,7 @@ const CategoryAttributeTemplatesWidget = ({
298
467
  size: "small",
299
468
  onClick: () => {
300
469
  setShowAddForm(false);
301
- setAddForm(emptyForm$1());
470
+ setAddForm(emptyForm$2());
302
471
  setMutationError(null);
303
472
  },
304
473
  children: "Отмена"
@@ -314,19 +483,17 @@ adminSdk.defineWidgetConfig({
314
483
  const ProductAttributeValuesWidget = ({
315
484
  data
316
485
  }) => {
317
- var _a, _b, _c;
486
+ var _a;
318
487
  const productId = data.id;
319
488
  const productHandle = data.handle || productId;
320
- const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
321
489
  const qc = reactQuery.useQueryClient();
322
490
  const [formValues, setFormValues] = React.useState({});
323
491
  const [saveSuccess, setSaveSuccess] = React.useState(false);
324
492
  const [uploadingId, setUploadingId] = React.useState(null);
325
493
  const fileInputs = React.useRef({});
326
494
  const attributesQuery = reactQuery.useQuery({
327
- queryKey: ["category-custom-attributes", categoryId],
328
- queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
329
- enabled: !!categoryId
495
+ queryKey: ["product-attribute-schema", productId],
496
+ queryFn: () => sdk.client.fetch(`/admin/product/${productId}/attribute-schema`)
330
497
  });
331
498
  const valuesQuery = reactQuery.useQuery({
332
499
  queryKey: ["product-custom-attributes", productId],
@@ -334,10 +501,10 @@ const ProductAttributeValuesWidget = ({
334
501
  });
335
502
  React.useEffect(() => {
336
503
  if (!attributesQuery.data || !valuesQuery.data) return;
337
- const attributes2 = attributesQuery.data.category_custom_attributes;
504
+ const attributes22 = attributesQuery.data.attributes;
338
505
  const values = valuesQuery.data.product_custom_attributes;
339
506
  const initial = {};
340
- for (const attr of attributes2) {
507
+ for (const attr of attributes22) {
341
508
  const existing = values.find(
342
509
  (v) => {
343
510
  var _a2;
@@ -363,24 +530,28 @@ const ProductAttributeValuesWidget = ({
363
530
  });
364
531
  const handleSave = () => {
365
532
  if (!attributesQuery.data) return;
366
- const attributes2 = attributesQuery.data.category_custom_attributes;
367
- const attributesToUpdate = attributes2.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
533
+ const attributes22 = attributesQuery.data.attributes;
534
+ const attributesToUpdate = attributes22.filter((attr) => formValues[attr.id] !== void 0).map((attr) => ({ id: attr.id, value: formValues[attr.id] ?? "" }));
368
535
  saveMutation.mutate({ attributes: attributesToUpdate });
369
536
  };
370
537
  const handleFileUpload = async (attr, file) => {
371
- var _a2, _b2;
538
+ var _a2, _b;
372
539
  const attrId = attr.id;
373
540
  setUploadingId(attrId);
374
541
  try {
375
- const dot = file.name.lastIndexOf(".");
376
- const ext = dot > -1 ? file.name.slice(dot) : "";
377
- const renamed = new File(
378
- [file],
379
- `${productHandle}_${attr.key}${ext}`,
380
- { type: file.type }
381
- );
542
+ const isImage = file.type.startsWith("image/");
543
+ let toUpload = file;
544
+ if (isImage) {
545
+ const dot = file.name.lastIndexOf(".");
546
+ const ext = dot > -1 ? file.name.slice(dot) : "";
547
+ toUpload = new File(
548
+ [file],
549
+ `${productHandle}_${attr.key}${ext}`,
550
+ { type: file.type }
551
+ );
552
+ }
382
553
  const formData = new FormData();
383
- formData.append("files", renamed);
554
+ formData.append("files", toUpload);
384
555
  const response = await fetch(`/admin/uploads`, {
385
556
  method: "POST",
386
557
  credentials: "include",
@@ -393,7 +564,7 @@ const ProductAttributeValuesWidget = ({
393
564
  return;
394
565
  }
395
566
  const res = await response.json();
396
- const url = (_b2 = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.url;
567
+ const url = (_b = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b.url;
397
568
  if (url) {
398
569
  setFormValues((prev) => ({ ...prev, [attrId]: url }));
399
570
  } else {
@@ -407,22 +578,16 @@ const ProductAttributeValuesWidget = ({
407
578
  setUploadingId(null);
408
579
  }
409
580
  };
410
- if (!categoryId) {
411
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
412
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
413
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
414
- ] });
415
- }
416
581
  const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
417
582
  const isError = attributesQuery.isError || valuesQuery.isError;
418
- const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
583
+ const attributes2 = ((_a = attributesQuery.data) == null ? void 0 : _a.attributes) ?? [];
419
584
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
420
585
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
421
586
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
422
587
  isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
423
- !isLoading && !isError && attributes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
424
- !isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
425
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
588
+ !isLoading && !isError && attributes2.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
589
+ !isLoading && !isError && attributes2.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
590
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes2.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
426
591
  "div",
427
592
  {
428
593
  className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
@@ -520,11 +685,76 @@ const ProductAttributeValuesWidget = ({
520
685
  adminSdk.defineWidgetConfig({
521
686
  zone: "product.details.after"
522
687
  });
688
+ var __defProp$1 = Object.defineProperty;
689
+ var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
690
+ var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
691
+ var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
692
+ var __defNormalProp$1 = (obj, key, value2) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
693
+ var __spreadValues$1 = (a, b) => {
694
+ for (var prop in b || (b = {}))
695
+ if (__hasOwnProp$1.call(b, prop))
696
+ __defNormalProp$1(a, prop, b[prop]);
697
+ if (__getOwnPropSymbols$1)
698
+ for (var prop of __getOwnPropSymbols$1(b)) {
699
+ if (__propIsEnum$1.call(b, prop))
700
+ __defNormalProp$1(a, prop, b[prop]);
701
+ }
702
+ return a;
703
+ };
704
+ var __objRest$1 = (source, exclude) => {
705
+ var target = {};
706
+ for (var prop in source)
707
+ if (__hasOwnProp$1.call(source, prop) && exclude.indexOf(prop) < 0)
708
+ target[prop] = source[prop];
709
+ if (source != null && __getOwnPropSymbols$1)
710
+ for (var prop of __getOwnPropSymbols$1(source)) {
711
+ if (exclude.indexOf(prop) < 0 && __propIsEnum$1.call(source, prop))
712
+ target[prop] = source[prop];
713
+ }
714
+ return target;
715
+ };
716
+ const Globe = React__namespace.forwardRef(
717
+ (_a, ref) => {
718
+ var _b = _a, { color = "currentColor" } = _b, props = __objRest$1(_b, ["color"]);
719
+ return /* @__PURE__ */ React__namespace.createElement(
720
+ "svg",
721
+ __spreadValues$1({
722
+ xmlns: "http://www.w3.org/2000/svg",
723
+ width: 15,
724
+ height: 15,
725
+ viewBox: "0 0 15 15",
726
+ fill: "none",
727
+ ref
728
+ }, props),
729
+ /* @__PURE__ */ React__namespace.createElement(
730
+ "path",
731
+ {
732
+ stroke: color,
733
+ strokeLinecap: "round",
734
+ strokeLinejoin: "round",
735
+ strokeWidth: 1.5,
736
+ 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"
737
+ }
738
+ ),
739
+ /* @__PURE__ */ React__namespace.createElement(
740
+ "path",
741
+ {
742
+ stroke: color,
743
+ strokeLinecap: "round",
744
+ strokeLinejoin: "round",
745
+ strokeWidth: 1.5,
746
+ d: "M7.5 13.945a6.444 6.444 0 1 0 0-12.89 6.444 6.444 0 0 0 0 12.89"
747
+ }
748
+ )
749
+ );
750
+ }
751
+ );
752
+ Globe.displayName = "Globe";
523
753
  var __defProp = Object.defineProperty;
524
754
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
525
755
  var __hasOwnProp = Object.prototype.hasOwnProperty;
526
756
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
527
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
757
+ var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
528
758
  var __spreadValues = (a, b) => {
529
759
  for (var prop in b || (b = {}))
530
760
  if (__hasOwnProp.call(b, prop))
@@ -548,7 +778,7 @@ var __objRest = (source, exclude) => {
548
778
  }
549
779
  return target;
550
780
  };
551
- const CogSixTooth = React__namespace.forwardRef(
781
+ const SquaresPlus = React__namespace.forwardRef(
552
782
  (_a, ref) => {
553
783
  var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
554
784
  return /* @__PURE__ */ React__namespace.createElement(
@@ -562,42 +792,39 @@ const CogSixTooth = React__namespace.forwardRef(
562
792
  ref
563
793
  }, props),
564
794
  /* @__PURE__ */ React__namespace.createElement(
565
- "g",
795
+ "path",
566
796
  {
567
797
  stroke: color,
568
798
  strokeLinecap: "round",
569
799
  strokeLinejoin: "round",
570
800
  strokeWidth: 1.5,
571
- clipPath: "url(#a)"
572
- },
573
- /* @__PURE__ */ React__namespace.createElement("path", { d: "M7.5 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4" }),
574
- /* @__PURE__ */ React__namespace.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" })
575
- ),
576
- /* @__PURE__ */ React__namespace.createElement("defs", null, /* @__PURE__ */ React__namespace.createElement("clipPath", { id: "a" }, /* @__PURE__ */ React__namespace.createElement("path", { fill: "#fff", d: "M0 0h15v15H0z" })))
801
+ 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"
802
+ }
803
+ )
577
804
  );
578
805
  }
579
806
  );
580
- CogSixTooth.displayName = "CogSixTooth";
581
- const emptyForm = () => ({
807
+ SquaresPlus.displayName = "SquaresPlus";
808
+ const emptyForm$1 = () => ({
582
809
  label: "",
583
810
  type: "text",
584
811
  unit: "",
585
812
  description: ""
586
813
  });
587
- const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
588
- const ProductAttributesSettingsPage = () => {
814
+ const typeLabel$1 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
815
+ const AttributeTemplatesSettingsPage = () => {
589
816
  const qc = reactQuery.useQueryClient();
590
- const queryKey = ["attribute-presets"];
817
+ const queryKey = ["attribute-templates"];
591
818
  const [showAddForm, setShowAddForm] = React.useState(false);
592
- const [addForm, setAddForm] = React.useState(emptyForm());
819
+ const [addForm, setAddForm] = React.useState(emptyForm$1());
593
820
  const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
594
821
  const { data, isLoading, isError } = reactQuery.useQuery({
595
822
  queryKey,
596
- queryFn: () => sdk.client.fetch(`/admin/attribute-presets`)
823
+ queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
597
824
  });
598
- const presets = (data == null ? void 0 : data.attribute_presets) ?? [];
825
+ const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
599
826
  const createMutation = reactQuery.useMutation({
600
- mutationFn: (body) => sdk.client.fetch(`/admin/attribute-presets`, {
827
+ mutationFn: (body) => sdk.client.fetch(`/admin/attribute-templates`, {
601
828
  method: "POST",
602
829
  body: {
603
830
  label: body.label,
@@ -609,11 +836,11 @@ const ProductAttributesSettingsPage = () => {
609
836
  onSuccess: () => {
610
837
  qc.invalidateQueries({ queryKey });
611
838
  setShowAddForm(false);
612
- setAddForm(emptyForm());
839
+ setAddForm(emptyForm$1());
613
840
  }
614
841
  });
615
842
  const deleteMutation = reactQuery.useMutation({
616
- mutationFn: (id) => sdk.client.fetch(`/admin/attribute-presets`, {
843
+ mutationFn: (id) => sdk.client.fetch(`/admin/attribute-templates`, {
617
844
  method: "PATCH",
618
845
  body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
619
846
  }),
@@ -634,154 +861,158 @@ const ProductAttributesSettingsPage = () => {
634
861
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
635
862
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
636
863
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
637
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Пресеты атрибутов" }),
638
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Глобальные шаблоны атрибутов, которые можно применить к любой категории." })
864
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Шаблоны атрибутов" }),
865
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Заготовки атрибутов, которые можно применить к любой категории." })
639
866
  ] }),
640
- !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(
641
- ui.Button,
642
- {
643
- variant: "secondary",
644
- size: "small",
645
- onClick: () => setShowAddForm(true),
646
- children: "+ Добавить пресет"
647
- }
648
- )
867
+ !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
649
868
  ] }),
650
869
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
651
- isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить пресеты." }) }),
652
- !isLoading && !isError && presets.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Пресетов пока нет. Добавьте первый." }) }),
653
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: presets.map(
654
- (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxRuntime.jsxs(
655
- "div",
656
- {
657
- className: "flex items-center gap-3 px-6 py-3 text-sm",
658
- children: [
659
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
660
- "Удалить «",
661
- p.label,
662
- "»?"
663
- ] }),
664
- /* @__PURE__ */ jsxRuntime.jsx(
665
- ui.Button,
666
- {
667
- size: "small",
668
- variant: "danger",
669
- onClick: () => deleteMutation.mutate(p.id),
670
- isLoading: deleteMutation.isPending,
671
- children: "Удалить"
672
- }
673
- ),
674
- /* @__PURE__ */ jsxRuntime.jsx(
675
- ui.Button,
676
- {
677
- size: "small",
678
- variant: "secondary",
679
- onClick: () => setConfirmDeleteId(null),
680
- children: "Отмена"
681
- }
682
- )
683
- ]
684
- },
685
- p.id
686
- ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
870
+ isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить шаблоны." }) }),
871
+ !isLoading && !isError && templates2.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Шаблонов пока нет." }) }),
872
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: templates2.map(
873
+ (p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
874
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
875
+ "Удалить «",
876
+ p.label,
877
+ "»?"
878
+ ] }),
879
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(p.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
880
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
881
+ ] }, p.id) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
687
882
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
688
883
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
689
884
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: p.label }),
690
885
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
691
- typeLabel(p.type),
886
+ typeLabel$1(p.type),
692
887
  p.unit ? `, ${p.unit}` : ""
693
888
  ] })
694
889
  ] }),
695
890
  p.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
696
891
  ] }),
697
- /* @__PURE__ */ jsxRuntime.jsx(
698
- "button",
699
- {
700
- onClick: () => setConfirmDeleteId(p.id),
701
- className: "text-xs text-ui-fg-error hover:underline",
702
- children: "Удалить"
703
- }
704
- )
892
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setConfirmDeleteId(p.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
705
893
  ] }, p.id)
706
894
  ) }),
707
895
  showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
708
896
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
709
- /* @__PURE__ */ jsxRuntime.jsx(
710
- ui.Input,
711
- {
712
- value: addForm.label,
713
- onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
714
- placeholder: "Название (например, Сертификат)",
715
- className: "flex-1 h-8 text-sm",
716
- autoFocus: true
717
- }
718
- ),
719
- /* @__PURE__ */ jsxRuntime.jsxs(
720
- "select",
721
- {
722
- value: addForm.type,
723
- onChange: (e) => setAddForm((f) => ({
724
- ...f,
725
- type: e.target.value,
726
- unit: e.target.value === "number" ? f.unit : ""
727
- })),
728
- className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
729
- children: [
730
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
731
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
732
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
733
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
734
- ]
735
- }
736
- ),
737
- addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(
738
- ui.Input,
739
- {
740
- value: addForm.unit,
741
- onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
742
- placeholder: "ед. (кг, м...)",
743
- className: "w-28 h-8 text-sm"
744
- }
745
- )
897
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
898
+ /* @__PURE__ */ jsxRuntime.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: [
899
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
900
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
901
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
902
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
903
+ ] }),
904
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" })
746
905
  ] }),
747
- /* @__PURE__ */ jsxRuntime.jsx(
748
- ui.Input,
749
- {
750
- value: addForm.description,
751
- onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
752
- placeholder: "Описание (необязательно)",
753
- className: "h-8 text-sm"
754
- }
755
- ),
906
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.description, onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })), placeholder: "Описание (необязательно)", className: "h-8 text-sm" }),
756
907
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
757
- /* @__PURE__ */ jsxRuntime.jsx(
758
- ui.Button,
759
- {
760
- variant: "secondary",
761
- size: "small",
762
- onClick: () => {
763
- setShowAddForm(false);
764
- setAddForm(emptyForm());
765
- },
766
- children: "Отмена"
767
- }
768
- ),
769
- /* @__PURE__ */ jsxRuntime.jsx(
770
- ui.Button,
771
- {
772
- size: "small",
773
- onClick: handleAdd,
774
- isLoading: createMutation.isPending,
775
- children: "Создать"
776
- }
777
- )
908
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => {
909
+ setShowAddForm(false);
910
+ setAddForm(emptyForm$1());
911
+ }, children: "Отмена" }),
912
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" })
778
913
  ] })
779
914
  ] })
780
915
  ] });
781
916
  };
917
+ const config$1 = adminSdk.defineRouteConfig({
918
+ label: "Attribute Templates",
919
+ icon: SquaresPlus
920
+ });
921
+ const emptyForm = () => ({ label: "", type: "text", unit: "" });
922
+ const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
923
+ const GlobalAttributesSettingsPage = () => {
924
+ const qc = reactQuery.useQueryClient();
925
+ const queryKey = ["global-attributes"];
926
+ const [showAddForm, setShowAddForm] = React.useState(false);
927
+ const [addForm, setAddForm] = React.useState(emptyForm());
928
+ const [confirmDeleteId, setConfirmDeleteId] = React.useState(null);
929
+ const { data, isLoading, isError } = reactQuery.useQuery({
930
+ queryKey,
931
+ queryFn: () => sdk.client.fetch(`/admin/global-attributes`)
932
+ });
933
+ const attrs = (data == null ? void 0 : data.global_attributes) ?? [];
934
+ const createMutation = reactQuery.useMutation({
935
+ mutationFn: (body) => sdk.client.fetch(`/admin/global-attributes`, {
936
+ method: "POST",
937
+ body: {
938
+ label: body.label,
939
+ type: body.type,
940
+ unit: body.type === "number" && body.unit ? body.unit : null
941
+ }
942
+ }),
943
+ onSuccess: () => {
944
+ qc.invalidateQueries({ queryKey });
945
+ setShowAddForm(false);
946
+ setAddForm(emptyForm());
947
+ }
948
+ });
949
+ const deleteMutation = reactQuery.useMutation({
950
+ mutationFn: (id) => sdk.client.fetch(`/admin/global-attributes`, {
951
+ method: "PATCH",
952
+ body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
953
+ }),
954
+ onSuccess: () => {
955
+ qc.invalidateQueries({ queryKey });
956
+ setConfirmDeleteId(null);
957
+ }
958
+ });
959
+ const handleAdd = () => {
960
+ if (!addForm.label.trim()) return;
961
+ createMutation.mutate({
962
+ label: addForm.label.trim(),
963
+ type: addForm.type,
964
+ unit: addForm.unit.trim()
965
+ });
966
+ };
967
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
968
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
969
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
970
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Глобальные атрибуты" }),
971
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Применяются ко всем товарам автоматически. Значения у продуктов необязательны." })
972
+ ] }),
973
+ !showAddForm && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
974
+ ] }),
975
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
976
+ isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить." }) }),
977
+ !isLoading && !isError && attrs.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Глобальных атрибутов нет." }) }),
978
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attrs.map(
979
+ (a) => confirmDeleteId === a.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
980
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
981
+ "Удалить «",
982
+ a.label,
983
+ "»?"
984
+ ] }),
985
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(a.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
986
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
987
+ ] }, a.id) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm", children: a.label }),
989
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", color: "grey", children: [
990
+ typeLabel(a.type),
991
+ a.unit ? `, ${a.unit}` : ""
992
+ ] }),
993
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setConfirmDeleteId(a.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
994
+ ] }, a.id)
995
+ ) }),
996
+ showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-4", children: [
997
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
998
+ /* @__PURE__ */ jsxRuntime.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: [
999
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
1000
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" }),
1001
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "file", children: "Файл" }),
1002
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "boolean", children: "Да/Нет" })
1003
+ ] }),
1004
+ addForm.type === "number" && /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" }),
1005
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" }),
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => {
1007
+ setShowAddForm(false);
1008
+ setAddForm(emptyForm());
1009
+ }, children: "Отмена" })
1010
+ ] })
1011
+ ] });
1012
+ };
782
1013
  const config = adminSdk.defineRouteConfig({
783
- label: "Product Attributes",
784
- icon: CogSixTooth
1014
+ label: "Global Attributes",
1015
+ icon: Globe
785
1016
  });
786
1017
  const widgetModule = { widgets: [
787
1018
  {
@@ -796,17 +1027,29 @@ const widgetModule = { widgets: [
796
1027
  const routeModule = {
797
1028
  routes: [
798
1029
  {
799
- Component: ProductAttributesSettingsPage,
800
- path: "/settings/product-attributes"
1030
+ Component: AttributeTemplatesSettingsPage,
1031
+ path: "/settings/attribute-templates"
1032
+ },
1033
+ {
1034
+ Component: GlobalAttributesSettingsPage,
1035
+ path: "/settings/global-attributes"
801
1036
  }
802
1037
  ]
803
1038
  };
804
1039
  const menuItemModule = {
805
1040
  menuItems: [
1041
+ {
1042
+ label: config$1.label,
1043
+ icon: config$1.icon,
1044
+ path: "/settings/attribute-templates",
1045
+ nested: void 0,
1046
+ rank: void 0,
1047
+ translationNs: void 0
1048
+ },
806
1049
  {
807
1050
  label: config.label,
808
1051
  icon: config.icon,
809
- path: "/settings/product-attributes",
1052
+ path: "/settings/global-attributes",
810
1053
  nested: void 0,
811
1054
  rank: void 0,
812
1055
  translationNs: void 0