@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.
- package/.medusa/server/src/admin/index.js +453 -210
- package/.medusa/server/src/admin/index.mjs +454 -211
- package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.js +1 -1
- package/.medusa/server/src/api/admin/attribute-templates/[id]/apply/route.js.map +1 -0
- package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.js +6 -6
- package/.medusa/server/src/api/admin/attribute-templates/route.js.map +1 -0
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +1 -1
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -1
- package/.medusa/server/src/api/admin/global-attributes/route.d.ts +4 -0
- package/.medusa/server/src/api/admin/global-attributes/route.js +30 -0
- package/.medusa/server/src/api/admin/global-attributes/route.js.map +1 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.d.ts +2 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js +34 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/index.d.ts +5 -5
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.d.ts +5 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js +58 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.d.ts → attribute-template.d.ts} +3 -3
- package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.js → attribute-template.js} +3 -3
- package/.medusa/server/src/modules/product-attributes/models/attribute-template.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.d.ts +2 -1
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js +2 -1
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -1
- package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.d.ts +2 -1
- package/.medusa/server/src/modules/product-attributes/service.d.ts +56 -17
- package/.medusa/server/src/modules/product-attributes/service.js +44 -18
- package/.medusa/server/src/modules/product-attributes/service.js.map +1 -1
- package/README.md +96 -312
- package/README.ru.md +85 -0
- package/package.json +1 -1
- package/.medusa/server/src/api/admin/attribute-presets/[id]/apply/route.js.map +0 -1
- package/.medusa/server/src/api/admin/attribute-presets/route.js.map +0 -1
- package/.medusa/server/src/modules/product-attributes/models/attribute-preset.js.map +0 -1
- /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.d.ts +0 -0
- /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
|
|
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 [
|
|
25
|
-
const [addForm, setAddForm] = useState(emptyForm$
|
|
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
|
|
29
|
-
queryKey: ["attribute-
|
|
30
|
-
queryFn: () => sdk.client.fetch(`/admin/attribute-
|
|
31
|
-
enabled:
|
|
194
|
+
const templatesQuery = useQuery({
|
|
195
|
+
queryKey: ["attribute-templates"],
|
|
196
|
+
queryFn: () => sdk.client.fetch(`/admin/attribute-templates`),
|
|
197
|
+
enabled: showTemplateList
|
|
32
198
|
});
|
|
33
|
-
const
|
|
34
|
-
mutationFn: (
|
|
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
|
-
|
|
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
|
|
55
|
-
const ownAttributes =
|
|
56
|
-
const inheritedAttributes =
|
|
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$
|
|
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 = (
|
|
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 && !
|
|
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: () =>
|
|
101
|
-
children: "
|
|
266
|
+
onClick: () => setShowTemplateList(true),
|
|
267
|
+
children: t("fromTemplate")
|
|
102
268
|
}
|
|
103
269
|
),
|
|
104
|
-
/* @__PURE__ */
|
|
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
|
-
|
|
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: () =>
|
|
292
|
+
onClick: () => setShowTemplateList(false),
|
|
124
293
|
children: "Закрыть"
|
|
125
294
|
}
|
|
126
295
|
)
|
|
127
296
|
] }),
|
|
128
|
-
|
|
129
|
-
((_a =
|
|
130
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: (_b =
|
|
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: () =>
|
|
134
|
-
disabled:
|
|
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 &&
|
|
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$
|
|
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
|
|
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: ["
|
|
308
|
-
queryFn: () => sdk.client.fetch(`/admin/
|
|
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
|
|
484
|
+
const attributes22 = attributesQuery.data.attributes;
|
|
318
485
|
const values = valuesQuery.data.product_custom_attributes;
|
|
319
486
|
const initial = {};
|
|
320
|
-
for (const attr of
|
|
487
|
+
for (const attr of attributes22) {
|
|
321
488
|
const existing = values.find(
|
|
322
489
|
(v) => {
|
|
323
490
|
var _a2;
|
|
@@ -343,24 +510,28 @@ const ProductAttributeValuesWidget = ({
|
|
|
343
510
|
});
|
|
344
511
|
const handleSave = () => {
|
|
345
512
|
if (!attributesQuery.data) return;
|
|
346
|
-
const
|
|
347
|
-
const attributesToUpdate =
|
|
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,
|
|
518
|
+
var _a2, _b;
|
|
352
519
|
const attrId = attr.id;
|
|
353
520
|
setUploadingId(attrId);
|
|
354
521
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
522
|
+
const isImage = file.type.startsWith("image/");
|
|
523
|
+
let toUpload = file;
|
|
524
|
+
if (isImage) {
|
|
525
|
+
const dot = file.name.lastIndexOf(".");
|
|
526
|
+
const ext = dot > -1 ? file.name.slice(dot) : "";
|
|
527
|
+
toUpload = new File(
|
|
528
|
+
[file],
|
|
529
|
+
`${productHandle}_${attr.key}${ext}`,
|
|
530
|
+
{ type: file.type }
|
|
531
|
+
);
|
|
532
|
+
}
|
|
362
533
|
const formData = new FormData();
|
|
363
|
-
formData.append("files",
|
|
534
|
+
formData.append("files", toUpload);
|
|
364
535
|
const response = await fetch(`/admin/uploads`, {
|
|
365
536
|
method: "POST",
|
|
366
537
|
credentials: "include",
|
|
@@ -373,7 +544,7 @@ const ProductAttributeValuesWidget = ({
|
|
|
373
544
|
return;
|
|
374
545
|
}
|
|
375
546
|
const res = await response.json();
|
|
376
|
-
const url = (
|
|
547
|
+
const url = (_b = (_a2 = res == null ? void 0 : res.files) == null ? void 0 : _a2[0]) == null ? void 0 : _b.url;
|
|
377
548
|
if (url) {
|
|
378
549
|
setFormValues((prev) => ({ ...prev, [attrId]: url }));
|
|
379
550
|
} else {
|
|
@@ -387,22 +558,16 @@ const ProductAttributeValuesWidget = ({
|
|
|
387
558
|
setUploadingId(null);
|
|
388
559
|
}
|
|
389
560
|
};
|
|
390
|
-
if (!categoryId) {
|
|
391
|
-
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
392
|
-
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
393
|
-
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
|
|
394
|
-
] });
|
|
395
|
-
}
|
|
396
561
|
const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
|
|
397
562
|
const isError = attributesQuery.isError || valuesQuery.isError;
|
|
398
|
-
const
|
|
563
|
+
const attributes2 = ((_a = attributesQuery.data) == null ? void 0 : _a.attributes) ?? [];
|
|
399
564
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
400
565
|
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
401
566
|
isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
402
567
|
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
|
|
403
|
-
!isLoading && !isError &&
|
|
404
|
-
!isLoading && !isError &&
|
|
405
|
-
/* @__PURE__ */ jsx("div", { className: "divide-y", children:
|
|
568
|
+
!isLoading && !isError && attributes2.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
|
|
569
|
+
!isLoading && !isError && attributes2.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
570
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes2.map((attr) => /* @__PURE__ */ jsxs(
|
|
406
571
|
"div",
|
|
407
572
|
{
|
|
408
573
|
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
@@ -500,11 +665,76 @@ const ProductAttributeValuesWidget = ({
|
|
|
500
665
|
defineWidgetConfig({
|
|
501
666
|
zone: "product.details.after"
|
|
502
667
|
});
|
|
668
|
+
var __defProp$1 = Object.defineProperty;
|
|
669
|
+
var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
|
|
670
|
+
var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
|
|
671
|
+
var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
|
|
672
|
+
var __defNormalProp$1 = (obj, key, value2) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
|
|
673
|
+
var __spreadValues$1 = (a, b) => {
|
|
674
|
+
for (var prop in b || (b = {}))
|
|
675
|
+
if (__hasOwnProp$1.call(b, prop))
|
|
676
|
+
__defNormalProp$1(a, prop, b[prop]);
|
|
677
|
+
if (__getOwnPropSymbols$1)
|
|
678
|
+
for (var prop of __getOwnPropSymbols$1(b)) {
|
|
679
|
+
if (__propIsEnum$1.call(b, prop))
|
|
680
|
+
__defNormalProp$1(a, prop, b[prop]);
|
|
681
|
+
}
|
|
682
|
+
return a;
|
|
683
|
+
};
|
|
684
|
+
var __objRest$1 = (source, exclude) => {
|
|
685
|
+
var target = {};
|
|
686
|
+
for (var prop in source)
|
|
687
|
+
if (__hasOwnProp$1.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
688
|
+
target[prop] = source[prop];
|
|
689
|
+
if (source != null && __getOwnPropSymbols$1)
|
|
690
|
+
for (var prop of __getOwnPropSymbols$1(source)) {
|
|
691
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum$1.call(source, prop))
|
|
692
|
+
target[prop] = source[prop];
|
|
693
|
+
}
|
|
694
|
+
return target;
|
|
695
|
+
};
|
|
696
|
+
const Globe = React.forwardRef(
|
|
697
|
+
(_a, ref) => {
|
|
698
|
+
var _b = _a, { color = "currentColor" } = _b, props = __objRest$1(_b, ["color"]);
|
|
699
|
+
return /* @__PURE__ */ React.createElement(
|
|
700
|
+
"svg",
|
|
701
|
+
__spreadValues$1({
|
|
702
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
703
|
+
width: 15,
|
|
704
|
+
height: 15,
|
|
705
|
+
viewBox: "0 0 15 15",
|
|
706
|
+
fill: "none",
|
|
707
|
+
ref
|
|
708
|
+
}, props),
|
|
709
|
+
/* @__PURE__ */ React.createElement(
|
|
710
|
+
"path",
|
|
711
|
+
{
|
|
712
|
+
stroke: color,
|
|
713
|
+
strokeLinecap: "round",
|
|
714
|
+
strokeLinejoin: "round",
|
|
715
|
+
strokeWidth: 1.5,
|
|
716
|
+
d: "M7.5 13.945c1.473 0 2.667-2.886 2.667-6.445S8.973 1.056 7.5 1.056 4.833 3.94 4.833 7.5s1.194 6.445 2.667 6.445M1.056 7.5h12.888"
|
|
717
|
+
}
|
|
718
|
+
),
|
|
719
|
+
/* @__PURE__ */ React.createElement(
|
|
720
|
+
"path",
|
|
721
|
+
{
|
|
722
|
+
stroke: color,
|
|
723
|
+
strokeLinecap: "round",
|
|
724
|
+
strokeLinejoin: "round",
|
|
725
|
+
strokeWidth: 1.5,
|
|
726
|
+
d: "M7.5 13.945a6.444 6.444 0 1 0 0-12.89 6.444 6.444 0 0 0 0 12.89"
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
Globe.displayName = "Globe";
|
|
503
733
|
var __defProp = Object.defineProperty;
|
|
504
734
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
505
735
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
506
736
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
507
|
-
var __defNormalProp = (obj, key,
|
|
737
|
+
var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
|
|
508
738
|
var __spreadValues = (a, b) => {
|
|
509
739
|
for (var prop in b || (b = {}))
|
|
510
740
|
if (__hasOwnProp.call(b, prop))
|
|
@@ -528,7 +758,7 @@ var __objRest = (source, exclude) => {
|
|
|
528
758
|
}
|
|
529
759
|
return target;
|
|
530
760
|
};
|
|
531
|
-
const
|
|
761
|
+
const SquaresPlus = React.forwardRef(
|
|
532
762
|
(_a, ref) => {
|
|
533
763
|
var _b = _a, { color = "currentColor" } = _b, props = __objRest(_b, ["color"]);
|
|
534
764
|
return /* @__PURE__ */ React.createElement(
|
|
@@ -542,42 +772,39 @@ const CogSixTooth = React.forwardRef(
|
|
|
542
772
|
ref
|
|
543
773
|
}, props),
|
|
544
774
|
/* @__PURE__ */ React.createElement(
|
|
545
|
-
"
|
|
775
|
+
"path",
|
|
546
776
|
{
|
|
547
777
|
stroke: color,
|
|
548
778
|
strokeLinecap: "round",
|
|
549
779
|
strokeLinejoin: "round",
|
|
550
780
|
strokeWidth: 1.5,
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/* @__PURE__ */ React.createElement("path", { d: "m12.989 5.97-.826-.292a5 5 0 0 0-.323-.685 5 5 0 0 0-.43-.621l.16-.86a1.43 1.43 0 0 0-.692-1.503l-.312-.18a1.43 1.43 0 0 0-1.647.152l-.663.566a5 5 0 0 0-1.513 0L6.08 1.98a1.43 1.43 0 0 0-1.647-.152l-.312.18a1.43 1.43 0 0 0-.691 1.503l.16.857c-.32.4-.574.841-.758 1.31l-.82.29a1.43 1.43 0 0 0-.956 1.35v.36c0 .608.383 1.15.955 1.35l.826.292c.09.232.194.462.323.684.128.222.275.427.43.622l-.16.86c-.111.597.166 1.2.691 1.503l.312.18a1.43 1.43 0 0 0 1.647-.152l.663-.567a5 5 0 0 0 1.512 0l.663.568a1.43 1.43 0 0 0 1.647.152l.312-.18c.526-.304.803-.906.691-1.502l-.16-.86c.32-.398.575-.84.757-1.308l.822-.29c.572-.202.956-.743.956-1.35v-.36c0-.608-.383-1.149-.956-1.35z" })
|
|
555
|
-
),
|
|
556
|
-
/* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("clipPath", { id: "a" }, /* @__PURE__ */ React.createElement("path", { fill: "#fff", d: "M0 0h15v15H0z" })))
|
|
781
|
+
d: "M5.056 1.944H2.833a.89.89 0 0 0-.889.89v2.221c0 .491.398.89.89.89h2.222c.49 0 .888-.399.888-.89V2.833a.89.89 0 0 0-.888-.889M12.167 1.944H9.944a.89.89 0 0 0-.888.89v2.221c0 .491.398.89.888.89h2.223c.49 0 .889-.399.889-.89V2.833a.89.89 0 0 0-.89-.889M5.056 9.056H2.833a.89.89 0 0 0-.889.889v2.222c0 .49.398.889.89.889h2.222c.49 0 .888-.398.888-.89V9.946a.89.89 0 0 0-.888-.89M11.056 8.611v4.445M13.278 10.833H8.833"
|
|
782
|
+
}
|
|
783
|
+
)
|
|
557
784
|
);
|
|
558
785
|
}
|
|
559
786
|
);
|
|
560
|
-
|
|
561
|
-
const emptyForm = () => ({
|
|
787
|
+
SquaresPlus.displayName = "SquaresPlus";
|
|
788
|
+
const emptyForm$1 = () => ({
|
|
562
789
|
label: "",
|
|
563
790
|
type: "text",
|
|
564
791
|
unit: "",
|
|
565
792
|
description: ""
|
|
566
793
|
});
|
|
567
|
-
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
|
|
568
|
-
const
|
|
794
|
+
const typeLabel$1 = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
|
|
795
|
+
const AttributeTemplatesSettingsPage = () => {
|
|
569
796
|
const qc = useQueryClient();
|
|
570
|
-
const queryKey = ["attribute-
|
|
797
|
+
const queryKey = ["attribute-templates"];
|
|
571
798
|
const [showAddForm, setShowAddForm] = useState(false);
|
|
572
|
-
const [addForm, setAddForm] = useState(emptyForm());
|
|
799
|
+
const [addForm, setAddForm] = useState(emptyForm$1());
|
|
573
800
|
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
|
574
801
|
const { data, isLoading, isError } = useQuery({
|
|
575
802
|
queryKey,
|
|
576
|
-
queryFn: () => sdk.client.fetch(`/admin/attribute-
|
|
803
|
+
queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
|
|
577
804
|
});
|
|
578
|
-
const
|
|
805
|
+
const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
|
|
579
806
|
const createMutation = useMutation({
|
|
580
|
-
mutationFn: (body) => sdk.client.fetch(`/admin/attribute-
|
|
807
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/attribute-templates`, {
|
|
581
808
|
method: "POST",
|
|
582
809
|
body: {
|
|
583
810
|
label: body.label,
|
|
@@ -589,11 +816,11 @@ const ProductAttributesSettingsPage = () => {
|
|
|
589
816
|
onSuccess: () => {
|
|
590
817
|
qc.invalidateQueries({ queryKey });
|
|
591
818
|
setShowAddForm(false);
|
|
592
|
-
setAddForm(emptyForm());
|
|
819
|
+
setAddForm(emptyForm$1());
|
|
593
820
|
}
|
|
594
821
|
});
|
|
595
822
|
const deleteMutation = useMutation({
|
|
596
|
-
mutationFn: (id) => sdk.client.fetch(`/admin/attribute-
|
|
823
|
+
mutationFn: (id) => sdk.client.fetch(`/admin/attribute-templates`, {
|
|
597
824
|
method: "PATCH",
|
|
598
825
|
body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
599
826
|
}),
|
|
@@ -614,154 +841,158 @@ const ProductAttributesSettingsPage = () => {
|
|
|
614
841
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
615
842
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
616
843
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
617
|
-
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "
|
|
618
|
-
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "
|
|
844
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Шаблоны атрибутов" }),
|
|
845
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Заготовки атрибутов, которые можно применить к любой категории." })
|
|
619
846
|
] }),
|
|
620
|
-
!showAddForm && /* @__PURE__ */ jsx(
|
|
621
|
-
Button,
|
|
622
|
-
{
|
|
623
|
-
variant: "secondary",
|
|
624
|
-
size: "small",
|
|
625
|
-
onClick: () => setShowAddForm(true),
|
|
626
|
-
children: "+ Добавить пресет"
|
|
627
|
-
}
|
|
628
|
-
)
|
|
847
|
+
!showAddForm && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
|
|
629
848
|
] }),
|
|
630
849
|
isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
631
|
-
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить
|
|
632
|
-
!isLoading && !isError &&
|
|
633
|
-
/* @__PURE__ */ jsx("div", { className: "divide-y", children:
|
|
634
|
-
(p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs(
|
|
635
|
-
"
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
] }),
|
|
644
|
-
/* @__PURE__ */ jsx(
|
|
645
|
-
Button,
|
|
646
|
-
{
|
|
647
|
-
size: "small",
|
|
648
|
-
variant: "danger",
|
|
649
|
-
onClick: () => deleteMutation.mutate(p.id),
|
|
650
|
-
isLoading: deleteMutation.isPending,
|
|
651
|
-
children: "Удалить"
|
|
652
|
-
}
|
|
653
|
-
),
|
|
654
|
-
/* @__PURE__ */ jsx(
|
|
655
|
-
Button,
|
|
656
|
-
{
|
|
657
|
-
size: "small",
|
|
658
|
-
variant: "secondary",
|
|
659
|
-
onClick: () => setConfirmDeleteId(null),
|
|
660
|
-
children: "Отмена"
|
|
661
|
-
}
|
|
662
|
-
)
|
|
663
|
-
]
|
|
664
|
-
},
|
|
665
|
-
p.id
|
|
666
|
-
) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
|
|
850
|
+
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить шаблоны." }) }),
|
|
851
|
+
!isLoading && !isError && templates2.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Шаблонов пока нет." }) }),
|
|
852
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y", children: templates2.map(
|
|
853
|
+
(p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
|
|
854
|
+
/* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
|
|
855
|
+
"Удалить «",
|
|
856
|
+
p.label,
|
|
857
|
+
"»?"
|
|
858
|
+
] }),
|
|
859
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(p.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
|
|
860
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
|
|
861
|
+
] }, p.id) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
|
|
667
862
|
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
668
863
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
669
864
|
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: p.label }),
|
|
670
865
|
/* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
|
|
671
|
-
typeLabel(p.type),
|
|
866
|
+
typeLabel$1(p.type),
|
|
672
867
|
p.unit ? `, ${p.unit}` : ""
|
|
673
868
|
] })
|
|
674
869
|
] }),
|
|
675
870
|
p.description && /* @__PURE__ */ jsx(Text, { size: "xsmall", className: "text-ui-fg-subtle", children: p.description })
|
|
676
871
|
] }),
|
|
677
|
-
/* @__PURE__ */ jsx(
|
|
678
|
-
"button",
|
|
679
|
-
{
|
|
680
|
-
onClick: () => setConfirmDeleteId(p.id),
|
|
681
|
-
className: "text-xs text-ui-fg-error hover:underline",
|
|
682
|
-
children: "Удалить"
|
|
683
|
-
}
|
|
684
|
-
)
|
|
872
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setConfirmDeleteId(p.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
|
|
685
873
|
] }, p.id)
|
|
686
874
|
) }),
|
|
687
875
|
showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-6 py-4", children: [
|
|
688
876
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
689
|
-
/* @__PURE__ */ jsx(
|
|
690
|
-
|
|
691
|
-
{
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
),
|
|
699
|
-
/* @__PURE__ */ jsxs(
|
|
700
|
-
"select",
|
|
701
|
-
{
|
|
702
|
-
value: addForm.type,
|
|
703
|
-
onChange: (e) => setAddForm((f) => ({
|
|
704
|
-
...f,
|
|
705
|
-
type: e.target.value,
|
|
706
|
-
unit: e.target.value === "number" ? f.unit : ""
|
|
707
|
-
})),
|
|
708
|
-
className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
709
|
-
children: [
|
|
710
|
-
/* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
|
|
711
|
-
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
|
|
712
|
-
/* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
|
|
713
|
-
/* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
|
|
714
|
-
]
|
|
715
|
-
}
|
|
716
|
-
),
|
|
717
|
-
addForm.type === "number" && /* @__PURE__ */ jsx(
|
|
718
|
-
Input,
|
|
719
|
-
{
|
|
720
|
-
value: addForm.unit,
|
|
721
|
-
onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })),
|
|
722
|
-
placeholder: "ед. (кг, м...)",
|
|
723
|
-
className: "w-28 h-8 text-sm"
|
|
724
|
-
}
|
|
725
|
-
)
|
|
877
|
+
/* @__PURE__ */ jsx(Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
|
|
878
|
+
/* @__PURE__ */ jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
|
|
879
|
+
/* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
|
|
880
|
+
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
|
|
881
|
+
/* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
|
|
882
|
+
/* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
|
|
883
|
+
] }),
|
|
884
|
+
addForm.type === "number" && /* @__PURE__ */ jsx(Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" })
|
|
726
885
|
] }),
|
|
727
|
-
/* @__PURE__ */ jsx(
|
|
728
|
-
Input,
|
|
729
|
-
{
|
|
730
|
-
value: addForm.description,
|
|
731
|
-
onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })),
|
|
732
|
-
placeholder: "Описание (необязательно)",
|
|
733
|
-
className: "h-8 text-sm"
|
|
734
|
-
}
|
|
735
|
-
),
|
|
886
|
+
/* @__PURE__ */ jsx(Input, { value: addForm.description, onChange: (e) => setAddForm((f) => ({ ...f, description: e.target.value })), placeholder: "Описание (необязательно)", className: "h-8 text-sm" }),
|
|
736
887
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
737
|
-
/* @__PURE__ */ jsx(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
onClick: () => {
|
|
743
|
-
setShowAddForm(false);
|
|
744
|
-
setAddForm(emptyForm());
|
|
745
|
-
},
|
|
746
|
-
children: "Отмена"
|
|
747
|
-
}
|
|
748
|
-
),
|
|
749
|
-
/* @__PURE__ */ jsx(
|
|
750
|
-
Button,
|
|
751
|
-
{
|
|
752
|
-
size: "small",
|
|
753
|
-
onClick: handleAdd,
|
|
754
|
-
isLoading: createMutation.isPending,
|
|
755
|
-
children: "Создать"
|
|
756
|
-
}
|
|
757
|
-
)
|
|
888
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => {
|
|
889
|
+
setShowAddForm(false);
|
|
890
|
+
setAddForm(emptyForm$1());
|
|
891
|
+
}, children: "Отмена" }),
|
|
892
|
+
/* @__PURE__ */ jsx(Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" })
|
|
758
893
|
] })
|
|
759
894
|
] })
|
|
760
895
|
] });
|
|
761
896
|
};
|
|
897
|
+
const config$1 = defineRouteConfig({
|
|
898
|
+
label: "Attribute Templates",
|
|
899
|
+
icon: SquaresPlus
|
|
900
|
+
});
|
|
901
|
+
const emptyForm = () => ({ label: "", type: "text", unit: "" });
|
|
902
|
+
const typeLabel = (t) => t === "text" ? "Текст" : t === "number" ? "Число" : t === "file" ? "Файл" : t === "boolean" ? "Да/Нет" : t;
|
|
903
|
+
const GlobalAttributesSettingsPage = () => {
|
|
904
|
+
const qc = useQueryClient();
|
|
905
|
+
const queryKey = ["global-attributes"];
|
|
906
|
+
const [showAddForm, setShowAddForm] = useState(false);
|
|
907
|
+
const [addForm, setAddForm] = useState(emptyForm());
|
|
908
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
|
909
|
+
const { data, isLoading, isError } = useQuery({
|
|
910
|
+
queryKey,
|
|
911
|
+
queryFn: () => sdk.client.fetch(`/admin/global-attributes`)
|
|
912
|
+
});
|
|
913
|
+
const attrs = (data == null ? void 0 : data.global_attributes) ?? [];
|
|
914
|
+
const createMutation = useMutation({
|
|
915
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/global-attributes`, {
|
|
916
|
+
method: "POST",
|
|
917
|
+
body: {
|
|
918
|
+
label: body.label,
|
|
919
|
+
type: body.type,
|
|
920
|
+
unit: body.type === "number" && body.unit ? body.unit : null
|
|
921
|
+
}
|
|
922
|
+
}),
|
|
923
|
+
onSuccess: () => {
|
|
924
|
+
qc.invalidateQueries({ queryKey });
|
|
925
|
+
setShowAddForm(false);
|
|
926
|
+
setAddForm(emptyForm());
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
const deleteMutation = useMutation({
|
|
930
|
+
mutationFn: (id) => sdk.client.fetch(`/admin/global-attributes`, {
|
|
931
|
+
method: "PATCH",
|
|
932
|
+
body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
933
|
+
}),
|
|
934
|
+
onSuccess: () => {
|
|
935
|
+
qc.invalidateQueries({ queryKey });
|
|
936
|
+
setConfirmDeleteId(null);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
const handleAdd = () => {
|
|
940
|
+
if (!addForm.label.trim()) return;
|
|
941
|
+
createMutation.mutate({
|
|
942
|
+
label: addForm.label.trim(),
|
|
943
|
+
type: addForm.type,
|
|
944
|
+
unit: addForm.unit.trim()
|
|
945
|
+
});
|
|
946
|
+
};
|
|
947
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
948
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
949
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
950
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Глобальные атрибуты" }),
|
|
951
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Применяются ко всем товарам автоматически. Значения у продуктов необязательны." })
|
|
952
|
+
] }),
|
|
953
|
+
!showAddForm && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowAddForm(true), children: "+ Добавить" })
|
|
954
|
+
] }),
|
|
955
|
+
isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
956
|
+
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить." }) }),
|
|
957
|
+
!isLoading && !isError && attrs.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Глобальных атрибутов нет." }) }),
|
|
958
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y", children: attrs.map(
|
|
959
|
+
(a) => confirmDeleteId === a.id ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3 text-sm", children: [
|
|
960
|
+
/* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
|
|
961
|
+
"Удалить «",
|
|
962
|
+
a.label,
|
|
963
|
+
"»?"
|
|
964
|
+
] }),
|
|
965
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", onClick: () => deleteMutation.mutate(a.id), isLoading: deleteMutation.isPending, children: "Удалить" }),
|
|
966
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => setConfirmDeleteId(null), children: "Отмена" })
|
|
967
|
+
] }, a.id) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-6 py-3", children: [
|
|
968
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm", children: a.label }),
|
|
969
|
+
/* @__PURE__ */ jsxs(Badge, { size: "2xsmall", color: "grey", children: [
|
|
970
|
+
typeLabel(a.type),
|
|
971
|
+
a.unit ? `, ${a.unit}` : ""
|
|
972
|
+
] }),
|
|
973
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setConfirmDeleteId(a.id), className: "text-xs text-ui-fg-error hover:underline", children: "Удалить" })
|
|
974
|
+
] }, a.id)
|
|
975
|
+
) }),
|
|
976
|
+
showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-6 py-4", children: [
|
|
977
|
+
/* @__PURE__ */ jsx(Input, { value: addForm.label, onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })), placeholder: "Название", className: "flex-1 h-8 text-sm", autoFocus: true }),
|
|
978
|
+
/* @__PURE__ */ jsxs("select", { value: addForm.type, onChange: (e) => setAddForm((f) => ({ ...f, type: e.target.value, unit: e.target.value === "number" ? f.unit : "" })), className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm", children: [
|
|
979
|
+
/* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
|
|
980
|
+
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" }),
|
|
981
|
+
/* @__PURE__ */ jsx("option", { value: "file", children: "Файл" }),
|
|
982
|
+
/* @__PURE__ */ jsx("option", { value: "boolean", children: "Да/Нет" })
|
|
983
|
+
] }),
|
|
984
|
+
addForm.type === "number" && /* @__PURE__ */ jsx(Input, { value: addForm.unit, onChange: (e) => setAddForm((f) => ({ ...f, unit: e.target.value })), placeholder: "ед.", className: "w-28 h-8 text-sm" }),
|
|
985
|
+
/* @__PURE__ */ jsx(Button, { size: "small", onClick: handleAdd, isLoading: createMutation.isPending, children: "Создать" }),
|
|
986
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => {
|
|
987
|
+
setShowAddForm(false);
|
|
988
|
+
setAddForm(emptyForm());
|
|
989
|
+
}, children: "Отмена" })
|
|
990
|
+
] })
|
|
991
|
+
] });
|
|
992
|
+
};
|
|
762
993
|
const config = defineRouteConfig({
|
|
763
|
-
label: "
|
|
764
|
-
icon:
|
|
994
|
+
label: "Global Attributes",
|
|
995
|
+
icon: Globe
|
|
765
996
|
});
|
|
766
997
|
const widgetModule = { widgets: [
|
|
767
998
|
{
|
|
@@ -776,17 +1007,29 @@ const widgetModule = { widgets: [
|
|
|
776
1007
|
const routeModule = {
|
|
777
1008
|
routes: [
|
|
778
1009
|
{
|
|
779
|
-
Component:
|
|
780
|
-
path: "/settings/
|
|
1010
|
+
Component: AttributeTemplatesSettingsPage,
|
|
1011
|
+
path: "/settings/attribute-templates"
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
Component: GlobalAttributesSettingsPage,
|
|
1015
|
+
path: "/settings/global-attributes"
|
|
781
1016
|
}
|
|
782
1017
|
]
|
|
783
1018
|
};
|
|
784
1019
|
const menuItemModule = {
|
|
785
1020
|
menuItems: [
|
|
1021
|
+
{
|
|
1022
|
+
label: config$1.label,
|
|
1023
|
+
icon: config$1.icon,
|
|
1024
|
+
path: "/settings/attribute-templates",
|
|
1025
|
+
nested: void 0,
|
|
1026
|
+
rank: void 0,
|
|
1027
|
+
translationNs: void 0
|
|
1028
|
+
},
|
|
786
1029
|
{
|
|
787
1030
|
label: config.label,
|
|
788
1031
|
icon: config.icon,
|
|
789
|
-
path: "/settings/
|
|
1032
|
+
path: "/settings/global-attributes",
|
|
790
1033
|
nested: void 0,
|
|
791
1034
|
rank: void 0,
|
|
792
1035
|
translationNs: void 0
|