@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
|
@@ -32,35 +32,201 @@ const sdk = new Medusa__default.default({
|
|
|
32
32
|
type: "session"
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
|
-
const
|
|
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 [
|
|
45
|
-
const [addForm, setAddForm] = React.useState(emptyForm$
|
|
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
|
|
49
|
-
queryKey: ["attribute-
|
|
50
|
-
queryFn: () => sdk.client.fetch(`/admin/attribute-
|
|
51
|
-
enabled:
|
|
214
|
+
const templatesQuery = reactQuery.useQuery({
|
|
215
|
+
queryKey: ["attribute-templates"],
|
|
216
|
+
queryFn: () => sdk.client.fetch(`/admin/attribute-templates`),
|
|
217
|
+
enabled: showTemplateList
|
|
52
218
|
});
|
|
53
|
-
const
|
|
54
|
-
mutationFn: (
|
|
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
|
-
|
|
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
|
|
75
|
-
const ownAttributes =
|
|
76
|
-
const inheritedAttributes =
|
|
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$
|
|
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 = (
|
|
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 && !
|
|
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: () =>
|
|
121
|
-
children: "
|
|
286
|
+
onClick: () => setShowTemplateList(true),
|
|
287
|
+
children: t("fromTemplate")
|
|
122
288
|
}
|
|
123
289
|
),
|
|
124
|
-
/* @__PURE__ */ jsxRuntime.
|
|
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
|
-
|
|
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: () =>
|
|
312
|
+
onClick: () => setShowTemplateList(false),
|
|
144
313
|
children: "Закрыть"
|
|
145
314
|
}
|
|
146
315
|
)
|
|
147
316
|
] }),
|
|
148
|
-
|
|
149
|
-
((_a =
|
|
150
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: (_b =
|
|
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: () =>
|
|
154
|
-
disabled:
|
|
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 &&
|
|
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$
|
|
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
|
|
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: ["
|
|
328
|
-
queryFn: () => sdk.client.fetch(`/admin/
|
|
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
|
|
504
|
+
const attributes22 = attributesQuery.data.attributes;
|
|
338
505
|
const values = valuesQuery.data.product_custom_attributes;
|
|
339
506
|
const initial = {};
|
|
340
|
-
for (const attr of
|
|
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
|
|
367
|
-
const attributesToUpdate =
|
|
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,
|
|
538
|
+
var _a2, _b;
|
|
372
539
|
const attrId = attr.id;
|
|
373
540
|
setUploadingId(attrId);
|
|
374
541
|
try {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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",
|
|
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 = (
|
|
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
|
|
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 &&
|
|
424
|
-
!isLoading && !isError &&
|
|
425
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children:
|
|
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,
|
|
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
|
|
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
|
-
"
|
|
795
|
+
"path",
|
|
566
796
|
{
|
|
567
797
|
stroke: color,
|
|
568
798
|
strokeLinecap: "round",
|
|
569
799
|
strokeLinejoin: "round",
|
|
570
800
|
strokeWidth: 1.5,
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
823
|
+
queryFn: () => sdk.client.fetch(`/admin/attribute-templates`)
|
|
597
824
|
});
|
|
598
|
-
const
|
|
825
|
+
const templates2 = (data == null ? void 0 : data.attribute_templates) ?? [];
|
|
599
826
|
const createMutation = reactQuery.useMutation({
|
|
600
|
-
mutationFn: (body) => sdk.client.fetch(`/admin/attribute-
|
|
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-
|
|
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 &&
|
|
653
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children:
|
|
654
|
-
(p) => confirmDeleteId === p.id ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
711
|
-
{
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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: "
|
|
784
|
-
icon:
|
|
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:
|
|
800
|
-
path: "/settings/
|
|
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/
|
|
1052
|
+
path: "/settings/global-attributes",
|
|
810
1053
|
nested: void 0,
|
|
811
1054
|
rank: void 0,
|
|
812
1055
|
translationNs: void 0
|