@empty-complete-org/medusa-product-attributes 0.10.0 → 0.11.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 +325 -0
- package/.medusa/server/src/admin/index.mjs +324 -0
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.d.ts +4 -0
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +32 -0
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -0
- package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.d.ts +3 -0
- package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.js +42 -0
- package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.js.map +1 -0
- package/{dist → .medusa/server/src/modules/product-attributes}/index.d.ts +0 -1
- package/.medusa/server/src/modules/product-attributes/index.js.map +1 -0
- package/{dist → .medusa/server/src/modules/product-attributes}/models/category-custom-attribute.d.ts +0 -1
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -0
- package/{dist → .medusa/server/src/modules/product-attributes}/models/product-custom-attribute.d.ts +0 -1
- package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.js.map +1 -0
- package/{dist → .medusa/server/src/modules/product-attributes}/service.d.ts +0 -1
- package/.medusa/server/src/modules/product-attributes/service.js.map +1 -0
- package/package.json +35 -22
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/models/category-custom-attribute.d.ts.map +0 -1
- package/dist/models/category-custom-attribute.js.map +0 -1
- package/dist/models/product-custom-attribute.d.ts.map +0 -1
- package/dist/models/product-custom-attribute.js.map +0 -1
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js.map +0 -1
- package/src/admin/lib/sdk.ts +0 -9
- package/src/admin/widgets/category-attribute-templates.tsx +0 -237
- package/src/admin/widgets/product-attribute-values.tsx +0 -186
- package/src/api/admin/category/[categoryId]/custom-attributes/route.ts +0 -49
- package/src/api/admin/product/[productId]/custom-attributes/route.ts +0 -51
- /package/{dist → .medusa/server/src/modules/product-attributes}/index.js +0 -0
- /package/{dist → .medusa/server/src/modules/product-attributes}/models/category-custom-attribute.js +0 -0
- /package/{dist → .medusa/server/src/modules/product-attributes}/models/product-custom-attribute.js +0 -0
- /package/{dist → .medusa/server/src/modules/product-attributes}/service.js +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
|
+
const ui = require("@medusajs/ui");
|
|
5
|
+
const reactQuery = require("@tanstack/react-query");
|
|
6
|
+
const react = require("react");
|
|
7
|
+
const Medusa = require("@medusajs/js-sdk");
|
|
8
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
9
|
+
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
10
|
+
const sdk = new Medusa__default.default({
|
|
11
|
+
baseUrl: "/",
|
|
12
|
+
debug: false,
|
|
13
|
+
auth: {
|
|
14
|
+
type: "session"
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const emptyForm = () => ({ label: "", type: "text" });
|
|
18
|
+
const CategoryAttributeTemplatesWidget = ({
|
|
19
|
+
data
|
|
20
|
+
}) => {
|
|
21
|
+
const categoryId = data.id;
|
|
22
|
+
const qc = reactQuery.useQueryClient();
|
|
23
|
+
const queryKey = ["category-custom-attributes", categoryId];
|
|
24
|
+
const [showAddForm, setShowAddForm] = react.useState(false);
|
|
25
|
+
const [addForm, setAddForm] = react.useState(emptyForm());
|
|
26
|
+
const [confirmDeleteId, setConfirmDeleteId] = react.useState(null);
|
|
27
|
+
const [mutationError, setMutationError] = react.useState(null);
|
|
28
|
+
const {
|
|
29
|
+
data: result,
|
|
30
|
+
isLoading,
|
|
31
|
+
isError
|
|
32
|
+
} = reactQuery.useQuery({
|
|
33
|
+
queryKey,
|
|
34
|
+
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
|
|
35
|
+
});
|
|
36
|
+
const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
|
|
37
|
+
const createMutation = reactQuery.useMutation({
|
|
38
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body
|
|
41
|
+
}),
|
|
42
|
+
onSuccess: () => {
|
|
43
|
+
qc.invalidateQueries({ queryKey });
|
|
44
|
+
setShowAddForm(false);
|
|
45
|
+
setAddForm(emptyForm());
|
|
46
|
+
setMutationError(null);
|
|
47
|
+
},
|
|
48
|
+
onError: (err) => {
|
|
49
|
+
setMutationError((err == null ? void 0 : err.message) || "Ошибка при создании атрибута");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const deleteMutation = reactQuery.useMutation({
|
|
53
|
+
mutationFn: (id) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
|
|
54
|
+
method: "PATCH",
|
|
55
|
+
body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
56
|
+
}),
|
|
57
|
+
onSuccess: () => {
|
|
58
|
+
qc.invalidateQueries({ queryKey });
|
|
59
|
+
setConfirmDeleteId(null);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const handleAdd = () => {
|
|
63
|
+
if (!addForm.label.trim()) return;
|
|
64
|
+
createMutation.mutate({
|
|
65
|
+
label: addForm.label.trim(),
|
|
66
|
+
type: addForm.type
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
70
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
71
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Атрибуты" }),
|
|
72
|
+
!showAddForm && /* @__PURE__ */ jsxRuntime.jsx(
|
|
73
|
+
ui.Button,
|
|
74
|
+
{
|
|
75
|
+
variant: "secondary",
|
|
76
|
+
size: "small",
|
|
77
|
+
onClick: () => setShowAddForm(true),
|
|
78
|
+
children: "+ Добавить"
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] }),
|
|
82
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
83
|
+
isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
|
|
84
|
+
!isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
|
|
85
|
+
attributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes.map(
|
|
86
|
+
(attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
87
|
+
"div",
|
|
88
|
+
{
|
|
89
|
+
className: "flex items-center gap-3 px-6 py-3 text-sm",
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
|
|
92
|
+
"Удалить «",
|
|
93
|
+
attr.label,
|
|
94
|
+
"»?"
|
|
95
|
+
] }),
|
|
96
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
ui.Button,
|
|
98
|
+
{
|
|
99
|
+
size: "small",
|
|
100
|
+
variant: "danger",
|
|
101
|
+
onClick: () => deleteMutation.mutate(attr.id),
|
|
102
|
+
isLoading: deleteMutation.isPending,
|
|
103
|
+
children: "Удалить"
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
107
|
+
ui.Button,
|
|
108
|
+
{
|
|
109
|
+
size: "small",
|
|
110
|
+
variant: "secondary",
|
|
111
|
+
onClick: () => setConfirmDeleteId(null),
|
|
112
|
+
children: "Отмена"
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
attr.id
|
|
118
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
119
|
+
"div",
|
|
120
|
+
{
|
|
121
|
+
className: "flex items-center gap-3 px-6 py-3",
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
124
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "grey", children: attr.type === "text" ? "Текст" : attr.type === "number" ? "Число" : attr.type }),
|
|
125
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
126
|
+
"button",
|
|
127
|
+
{
|
|
128
|
+
onClick: () => setConfirmDeleteId(attr.id),
|
|
129
|
+
className: "text-xs text-ui-fg-error hover:underline",
|
|
130
|
+
children: "Удалить"
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
attr.id
|
|
136
|
+
)
|
|
137
|
+
) }),
|
|
138
|
+
showAddForm && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
140
|
+
ui.Input,
|
|
141
|
+
{
|
|
142
|
+
value: addForm.label,
|
|
143
|
+
onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
|
|
144
|
+
placeholder: "Название атрибута",
|
|
145
|
+
className: "flex-1 h-8 text-sm",
|
|
146
|
+
autoFocus: true
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
150
|
+
"select",
|
|
151
|
+
{
|
|
152
|
+
value: addForm.type,
|
|
153
|
+
onChange: (e) => setAddForm((f) => ({
|
|
154
|
+
...f,
|
|
155
|
+
type: e.target.value
|
|
156
|
+
})),
|
|
157
|
+
className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "text", children: "Текст" }),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "number", children: "Число" })
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
165
|
+
ui.Button,
|
|
166
|
+
{
|
|
167
|
+
size: "small",
|
|
168
|
+
onClick: handleAdd,
|
|
169
|
+
isLoading: createMutation.isPending,
|
|
170
|
+
children: "Добавить"
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
|
+
ui.Button,
|
|
175
|
+
{
|
|
176
|
+
variant: "secondary",
|
|
177
|
+
size: "small",
|
|
178
|
+
onClick: () => {
|
|
179
|
+
setShowAddForm(false);
|
|
180
|
+
setAddForm(emptyForm());
|
|
181
|
+
setMutationError(null);
|
|
182
|
+
},
|
|
183
|
+
children: "Отмена"
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
] }),
|
|
187
|
+
mutationError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: mutationError }) })
|
|
188
|
+
] });
|
|
189
|
+
};
|
|
190
|
+
adminSdk.defineWidgetConfig({
|
|
191
|
+
zone: "product_category.details.after"
|
|
192
|
+
});
|
|
193
|
+
const ProductAttributeValuesWidget = ({
|
|
194
|
+
data
|
|
195
|
+
}) => {
|
|
196
|
+
var _a, _b, _c;
|
|
197
|
+
const productId = data.id;
|
|
198
|
+
const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
|
|
199
|
+
const qc = reactQuery.useQueryClient();
|
|
200
|
+
const [formValues, setFormValues] = react.useState({});
|
|
201
|
+
const [saveSuccess, setSaveSuccess] = react.useState(false);
|
|
202
|
+
const attributesQuery = reactQuery.useQuery({
|
|
203
|
+
queryKey: ["category-custom-attributes", categoryId],
|
|
204
|
+
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
205
|
+
enabled: !!categoryId
|
|
206
|
+
});
|
|
207
|
+
const valuesQuery = reactQuery.useQuery({
|
|
208
|
+
queryKey: ["product-custom-attributes", productId],
|
|
209
|
+
queryFn: () => sdk.client.fetch(`/admin/product/${productId}/custom-attributes`)
|
|
210
|
+
});
|
|
211
|
+
react.useEffect(() => {
|
|
212
|
+
if (!attributesQuery.data || !valuesQuery.data) return;
|
|
213
|
+
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
214
|
+
const values = valuesQuery.data.product_custom_attributes;
|
|
215
|
+
const initial = {};
|
|
216
|
+
for (const attr of attributes2) {
|
|
217
|
+
const existing = values.find((v) => v.id === attr.id);
|
|
218
|
+
initial[attr.id] = existing ? existing.value : "";
|
|
219
|
+
}
|
|
220
|
+
setFormValues(initial);
|
|
221
|
+
}, [attributesQuery.data, valuesQuery.data]);
|
|
222
|
+
const saveMutation = reactQuery.useMutation({
|
|
223
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/product/${productId}/custom-attributes`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body
|
|
226
|
+
}),
|
|
227
|
+
onSuccess: () => {
|
|
228
|
+
qc.invalidateQueries({
|
|
229
|
+
queryKey: ["product-custom-attributes", productId]
|
|
230
|
+
});
|
|
231
|
+
setSaveSuccess(true);
|
|
232
|
+
setTimeout(() => setSaveSuccess(false), 2e3);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const handleSave = () => {
|
|
236
|
+
if (!attributesQuery.data) return;
|
|
237
|
+
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
238
|
+
const attributesToUpdate = attributes2.filter(
|
|
239
|
+
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
240
|
+
).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
|
|
241
|
+
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
242
|
+
};
|
|
243
|
+
if (!categoryId) {
|
|
244
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
245
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
|
|
246
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
|
|
247
|
+
] });
|
|
248
|
+
}
|
|
249
|
+
const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
|
|
250
|
+
const isError = attributesQuery.isError || valuesQuery.isError;
|
|
251
|
+
const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
|
|
252
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
253
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Характеристики" }) }),
|
|
254
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
255
|
+
isError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
|
|
256
|
+
!isLoading && !isError && attributes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
|
|
257
|
+
!isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y", children: attributes.map((attr) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
259
|
+
"div",
|
|
260
|
+
{
|
|
261
|
+
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
262
|
+
children: [
|
|
263
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
264
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
265
|
+
ui.Input,
|
|
266
|
+
{
|
|
267
|
+
type: attr.type === "number" ? "number" : "text",
|
|
268
|
+
value: formValues[attr.id] ?? "",
|
|
269
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
270
|
+
...prev,
|
|
271
|
+
[attr.id]: e.target.value
|
|
272
|
+
})),
|
|
273
|
+
placeholder: attr.type === "number" ? "0" : "Значение",
|
|
274
|
+
className: "h-8 text-sm flex-1"
|
|
275
|
+
}
|
|
276
|
+
) })
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
attr.id
|
|
280
|
+
)) }),
|
|
281
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
282
|
+
ui.Button,
|
|
283
|
+
{
|
|
284
|
+
size: "small",
|
|
285
|
+
onClick: handleSave,
|
|
286
|
+
isLoading: saveMutation.isPending,
|
|
287
|
+
children: saveSuccess ? "Сохранено ✓" : "Сохранить"
|
|
288
|
+
}
|
|
289
|
+
) })
|
|
290
|
+
] })
|
|
291
|
+
] });
|
|
292
|
+
};
|
|
293
|
+
adminSdk.defineWidgetConfig({
|
|
294
|
+
zone: "product.details.after"
|
|
295
|
+
});
|
|
296
|
+
const widgetModule = { widgets: [
|
|
297
|
+
{
|
|
298
|
+
Component: CategoryAttributeTemplatesWidget,
|
|
299
|
+
zone: ["product_category.details.after"]
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
Component: ProductAttributeValuesWidget,
|
|
303
|
+
zone: ["product.details.after"]
|
|
304
|
+
}
|
|
305
|
+
] };
|
|
306
|
+
const routeModule = {
|
|
307
|
+
routes: []
|
|
308
|
+
};
|
|
309
|
+
const menuItemModule = {
|
|
310
|
+
menuItems: []
|
|
311
|
+
};
|
|
312
|
+
const formModule = { customFields: {} };
|
|
313
|
+
const displayModule = {
|
|
314
|
+
displays: {}
|
|
315
|
+
};
|
|
316
|
+
const i18nModule = { resources: {} };
|
|
317
|
+
const plugin = {
|
|
318
|
+
widgetModule,
|
|
319
|
+
routeModule,
|
|
320
|
+
menuItemModule,
|
|
321
|
+
formModule,
|
|
322
|
+
displayModule,
|
|
323
|
+
i18nModule
|
|
324
|
+
};
|
|
325
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
3
|
+
import { Container, Heading, Button, Text, Badge, Input } from "@medusajs/ui";
|
|
4
|
+
import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
import Medusa from "@medusajs/js-sdk";
|
|
7
|
+
const sdk = new Medusa({
|
|
8
|
+
baseUrl: "/",
|
|
9
|
+
debug: false,
|
|
10
|
+
auth: {
|
|
11
|
+
type: "session"
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const emptyForm = () => ({ label: "", type: "text" });
|
|
15
|
+
const CategoryAttributeTemplatesWidget = ({
|
|
16
|
+
data
|
|
17
|
+
}) => {
|
|
18
|
+
const categoryId = data.id;
|
|
19
|
+
const qc = useQueryClient();
|
|
20
|
+
const queryKey = ["category-custom-attributes", categoryId];
|
|
21
|
+
const [showAddForm, setShowAddForm] = useState(false);
|
|
22
|
+
const [addForm, setAddForm] = useState(emptyForm());
|
|
23
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
|
24
|
+
const [mutationError, setMutationError] = useState(null);
|
|
25
|
+
const {
|
|
26
|
+
data: result,
|
|
27
|
+
isLoading,
|
|
28
|
+
isError
|
|
29
|
+
} = useQuery({
|
|
30
|
+
queryKey,
|
|
31
|
+
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`)
|
|
32
|
+
});
|
|
33
|
+
const attributes = (result == null ? void 0 : result.category_custom_attributes) ?? [];
|
|
34
|
+
const createMutation = useMutation({
|
|
35
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
body
|
|
38
|
+
}),
|
|
39
|
+
onSuccess: () => {
|
|
40
|
+
qc.invalidateQueries({ queryKey });
|
|
41
|
+
setShowAddForm(false);
|
|
42
|
+
setAddForm(emptyForm());
|
|
43
|
+
setMutationError(null);
|
|
44
|
+
},
|
|
45
|
+
onError: (err) => {
|
|
46
|
+
setMutationError((err == null ? void 0 : err.message) || "Ошибка при создании атрибута");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const deleteMutation = useMutation({
|
|
50
|
+
mutationFn: (id) => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
|
|
51
|
+
method: "PATCH",
|
|
52
|
+
body: { id, deleted_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
53
|
+
}),
|
|
54
|
+
onSuccess: () => {
|
|
55
|
+
qc.invalidateQueries({ queryKey });
|
|
56
|
+
setConfirmDeleteId(null);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const handleAdd = () => {
|
|
60
|
+
if (!addForm.label.trim()) return;
|
|
61
|
+
createMutation.mutate({
|
|
62
|
+
label: addForm.label.trim(),
|
|
63
|
+
type: addForm.type
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
67
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
68
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Атрибуты" }),
|
|
69
|
+
!showAddForm && /* @__PURE__ */ jsx(
|
|
70
|
+
Button,
|
|
71
|
+
{
|
|
72
|
+
variant: "secondary",
|
|
73
|
+
size: "small",
|
|
74
|
+
onClick: () => setShowAddForm(true),
|
|
75
|
+
children: "+ Добавить"
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
] }),
|
|
79
|
+
isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
80
|
+
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить атрибуты." }) }),
|
|
81
|
+
!isLoading && !isError && attributes.length === 0 && !showAddForm && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Нет атрибутов. Добавьте первый." }) }),
|
|
82
|
+
attributes.length > 0 && /* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes.map(
|
|
83
|
+
(attr) => confirmDeleteId === attr.id ? /* @__PURE__ */ jsxs(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
className: "flex items-center gap-3 px-6 py-3 text-sm",
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ jsxs("span", { className: "flex-1 text-ui-fg-base", children: [
|
|
89
|
+
"Удалить «",
|
|
90
|
+
attr.label,
|
|
91
|
+
"»?"
|
|
92
|
+
] }),
|
|
93
|
+
/* @__PURE__ */ jsx(
|
|
94
|
+
Button,
|
|
95
|
+
{
|
|
96
|
+
size: "small",
|
|
97
|
+
variant: "danger",
|
|
98
|
+
onClick: () => deleteMutation.mutate(attr.id),
|
|
99
|
+
isLoading: deleteMutation.isPending,
|
|
100
|
+
children: "Удалить"
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
Button,
|
|
105
|
+
{
|
|
106
|
+
size: "small",
|
|
107
|
+
variant: "secondary",
|
|
108
|
+
onClick: () => setConfirmDeleteId(null),
|
|
109
|
+
children: "Отмена"
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
attr.id
|
|
115
|
+
) : /* @__PURE__ */ jsxs(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className: "flex items-center gap-3 px-6 py-3",
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-ui-fg-base", children: attr.label }),
|
|
121
|
+
/* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "grey", children: attr.type === "text" ? "Текст" : attr.type === "number" ? "Число" : attr.type }),
|
|
122
|
+
/* @__PURE__ */ jsx(
|
|
123
|
+
"button",
|
|
124
|
+
{
|
|
125
|
+
onClick: () => setConfirmDeleteId(attr.id),
|
|
126
|
+
className: "text-xs text-ui-fg-error hover:underline",
|
|
127
|
+
children: "Удалить"
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
attr.id
|
|
133
|
+
)
|
|
134
|
+
) }),
|
|
135
|
+
showAddForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-6 py-3", children: [
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
Input,
|
|
138
|
+
{
|
|
139
|
+
value: addForm.label,
|
|
140
|
+
onChange: (e) => setAddForm((f) => ({ ...f, label: e.target.value })),
|
|
141
|
+
placeholder: "Название атрибута",
|
|
142
|
+
className: "flex-1 h-8 text-sm",
|
|
143
|
+
autoFocus: true
|
|
144
|
+
}
|
|
145
|
+
),
|
|
146
|
+
/* @__PURE__ */ jsxs(
|
|
147
|
+
"select",
|
|
148
|
+
{
|
|
149
|
+
value: addForm.type,
|
|
150
|
+
onChange: (e) => setAddForm((f) => ({
|
|
151
|
+
...f,
|
|
152
|
+
type: e.target.value
|
|
153
|
+
})),
|
|
154
|
+
className: "h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx("option", { value: "text", children: "Текст" }),
|
|
157
|
+
/* @__PURE__ */ jsx("option", { value: "number", children: "Число" })
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
Button,
|
|
163
|
+
{
|
|
164
|
+
size: "small",
|
|
165
|
+
onClick: handleAdd,
|
|
166
|
+
isLoading: createMutation.isPending,
|
|
167
|
+
children: "Добавить"
|
|
168
|
+
}
|
|
169
|
+
),
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
Button,
|
|
172
|
+
{
|
|
173
|
+
variant: "secondary",
|
|
174
|
+
size: "small",
|
|
175
|
+
onClick: () => {
|
|
176
|
+
setShowAddForm(false);
|
|
177
|
+
setAddForm(emptyForm());
|
|
178
|
+
setMutationError(null);
|
|
179
|
+
},
|
|
180
|
+
children: "Отмена"
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
] }),
|
|
184
|
+
mutationError && /* @__PURE__ */ jsx("div", { className: "px-6 py-2", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: mutationError }) })
|
|
185
|
+
] });
|
|
186
|
+
};
|
|
187
|
+
defineWidgetConfig({
|
|
188
|
+
zone: "product_category.details.after"
|
|
189
|
+
});
|
|
190
|
+
const ProductAttributeValuesWidget = ({
|
|
191
|
+
data
|
|
192
|
+
}) => {
|
|
193
|
+
var _a, _b, _c;
|
|
194
|
+
const productId = data.id;
|
|
195
|
+
const categoryId = ((_b = (_a = data.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.id) ?? null;
|
|
196
|
+
const qc = useQueryClient();
|
|
197
|
+
const [formValues, setFormValues] = useState({});
|
|
198
|
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
199
|
+
const attributesQuery = useQuery({
|
|
200
|
+
queryKey: ["category-custom-attributes", categoryId],
|
|
201
|
+
queryFn: () => sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
|
|
202
|
+
enabled: !!categoryId
|
|
203
|
+
});
|
|
204
|
+
const valuesQuery = useQuery({
|
|
205
|
+
queryKey: ["product-custom-attributes", productId],
|
|
206
|
+
queryFn: () => sdk.client.fetch(`/admin/product/${productId}/custom-attributes`)
|
|
207
|
+
});
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!attributesQuery.data || !valuesQuery.data) return;
|
|
210
|
+
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
211
|
+
const values = valuesQuery.data.product_custom_attributes;
|
|
212
|
+
const initial = {};
|
|
213
|
+
for (const attr of attributes2) {
|
|
214
|
+
const existing = values.find((v) => v.id === attr.id);
|
|
215
|
+
initial[attr.id] = existing ? existing.value : "";
|
|
216
|
+
}
|
|
217
|
+
setFormValues(initial);
|
|
218
|
+
}, [attributesQuery.data, valuesQuery.data]);
|
|
219
|
+
const saveMutation = useMutation({
|
|
220
|
+
mutationFn: (body) => sdk.client.fetch(`/admin/product/${productId}/custom-attributes`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
body
|
|
223
|
+
}),
|
|
224
|
+
onSuccess: () => {
|
|
225
|
+
qc.invalidateQueries({
|
|
226
|
+
queryKey: ["product-custom-attributes", productId]
|
|
227
|
+
});
|
|
228
|
+
setSaveSuccess(true);
|
|
229
|
+
setTimeout(() => setSaveSuccess(false), 2e3);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const handleSave = () => {
|
|
233
|
+
if (!attributesQuery.data) return;
|
|
234
|
+
const attributes2 = attributesQuery.data.category_custom_attributes;
|
|
235
|
+
const attributesToUpdate = attributes2.filter(
|
|
236
|
+
(attr) => formValues[attr.id] !== void 0 && formValues[attr.id] !== ""
|
|
237
|
+
).map((attr) => ({ id: attr.id, value: formValues[attr.id] }));
|
|
238
|
+
saveMutation.mutate({ attributes: attributesToUpdate });
|
|
239
|
+
};
|
|
240
|
+
if (!categoryId) {
|
|
241
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
242
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
243
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Назначьте категорию товару, чтобы заполнить характеристики." }) })
|
|
244
|
+
] });
|
|
245
|
+
}
|
|
246
|
+
const isLoading = attributesQuery.isLoading || valuesQuery.isLoading;
|
|
247
|
+
const isError = attributesQuery.isError || valuesQuery.isError;
|
|
248
|
+
const attributes = ((_c = attributesQuery.data) == null ? void 0 : _c.category_custom_attributes) ?? [];
|
|
249
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
250
|
+
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Характеристики" }) }),
|
|
251
|
+
isLoading && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Загрузка…" }) }),
|
|
252
|
+
isError && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-error text-sm", children: "Не удалось загрузить характеристики." }) }),
|
|
253
|
+
!isLoading && !isError && attributes.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "В категории нет атрибутов. Добавьте их в настройках категории." }) }),
|
|
254
|
+
!isLoading && !isError && attributes.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
255
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y", children: attributes.map((attr) => /* @__PURE__ */ jsxs(
|
|
256
|
+
"div",
|
|
257
|
+
{
|
|
258
|
+
className: "grid grid-cols-2 items-center gap-4 px-6 py-3",
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: attr.label }),
|
|
261
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(
|
|
262
|
+
Input,
|
|
263
|
+
{
|
|
264
|
+
type: attr.type === "number" ? "number" : "text",
|
|
265
|
+
value: formValues[attr.id] ?? "",
|
|
266
|
+
onChange: (e) => setFormValues((prev) => ({
|
|
267
|
+
...prev,
|
|
268
|
+
[attr.id]: e.target.value
|
|
269
|
+
})),
|
|
270
|
+
placeholder: attr.type === "number" ? "0" : "Значение",
|
|
271
|
+
className: "h-8 text-sm flex-1"
|
|
272
|
+
}
|
|
273
|
+
) })
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
attr.id
|
|
277
|
+
)) }),
|
|
278
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-end px-6 py-4", children: /* @__PURE__ */ jsx(
|
|
279
|
+
Button,
|
|
280
|
+
{
|
|
281
|
+
size: "small",
|
|
282
|
+
onClick: handleSave,
|
|
283
|
+
isLoading: saveMutation.isPending,
|
|
284
|
+
children: saveSuccess ? "Сохранено ✓" : "Сохранить"
|
|
285
|
+
}
|
|
286
|
+
) })
|
|
287
|
+
] })
|
|
288
|
+
] });
|
|
289
|
+
};
|
|
290
|
+
defineWidgetConfig({
|
|
291
|
+
zone: "product.details.after"
|
|
292
|
+
});
|
|
293
|
+
const widgetModule = { widgets: [
|
|
294
|
+
{
|
|
295
|
+
Component: CategoryAttributeTemplatesWidget,
|
|
296
|
+
zone: ["product_category.details.after"]
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
Component: ProductAttributeValuesWidget,
|
|
300
|
+
zone: ["product.details.after"]
|
|
301
|
+
}
|
|
302
|
+
] };
|
|
303
|
+
const routeModule = {
|
|
304
|
+
routes: []
|
|
305
|
+
};
|
|
306
|
+
const menuItemModule = {
|
|
307
|
+
menuItems: []
|
|
308
|
+
};
|
|
309
|
+
const formModule = { customFields: {} };
|
|
310
|
+
const displayModule = {
|
|
311
|
+
displays: {}
|
|
312
|
+
};
|
|
313
|
+
const i18nModule = { resources: {} };
|
|
314
|
+
const plugin = {
|
|
315
|
+
widgetModule,
|
|
316
|
+
routeModule,
|
|
317
|
+
menuItemModule,
|
|
318
|
+
formModule,
|
|
319
|
+
displayModule,
|
|
320
|
+
i18nModule
|
|
321
|
+
};
|
|
322
|
+
export {
|
|
323
|
+
plugin as default
|
|
324
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
|
|
2
|
+
export declare function GET(req: MedusaRequest, res: MedusaResponse): Promise<void>;
|
|
3
|
+
export declare function POST(req: MedusaRequest, res: MedusaResponse): Promise<void>;
|
|
4
|
+
export declare function PATCH(req: MedusaRequest, res: MedusaResponse): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
exports.POST = POST;
|
|
5
|
+
exports.PATCH = PATCH;
|
|
6
|
+
const product_attributes_1 = require("../../../../../modules/product-attributes");
|
|
7
|
+
async function GET(req, res) {
|
|
8
|
+
const { categoryId } = req.params;
|
|
9
|
+
const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
|
|
10
|
+
const category_custom_attributes = await service.getCategoryAttributes(categoryId);
|
|
11
|
+
res.json({ category_custom_attributes });
|
|
12
|
+
}
|
|
13
|
+
async function POST(req, res) {
|
|
14
|
+
const { categoryId } = req.params;
|
|
15
|
+
const { label, type, unit, sort_order } = req.body;
|
|
16
|
+
const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
|
|
17
|
+
const category_custom_attribute = await service.createCategoryAttribute({
|
|
18
|
+
label,
|
|
19
|
+
type: type || "text",
|
|
20
|
+
unit: unit || null,
|
|
21
|
+
category_id: categoryId,
|
|
22
|
+
sort_order,
|
|
23
|
+
});
|
|
24
|
+
res.status(201).json({ category_custom_attribute });
|
|
25
|
+
}
|
|
26
|
+
async function PATCH(req, res) {
|
|
27
|
+
const service = req.scope.resolve(product_attributes_1.CUSTOM_ATTRIBUTE_MODULE);
|
|
28
|
+
const { id, ...data } = req.body;
|
|
29
|
+
const category_custom_attribute = await service.updateCategoryAttribute(id, data);
|
|
30
|
+
res.json({ category_custom_attribute });
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=route.js.map
|