@empty-complete-org/medusa-product-attributes 0.10.1 → 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.
Files changed (34) hide show
  1. package/.medusa/server/src/admin/index.js +325 -0
  2. package/.medusa/server/src/admin/index.mjs +324 -0
  3. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.d.ts +4 -0
  4. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +32 -0
  5. package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -0
  6. package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.d.ts +3 -0
  7. package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.js +42 -0
  8. package/.medusa/server/src/api/admin/product/[productId]/custom-attributes/route.js.map +1 -0
  9. package/{dist → .medusa/server/src/modules/product-attributes}/index.d.ts +0 -1
  10. package/.medusa/server/src/modules/product-attributes/index.js.map +1 -0
  11. package/{dist → .medusa/server/src/modules/product-attributes}/models/category-custom-attribute.d.ts +0 -1
  12. package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -0
  13. package/{dist → .medusa/server/src/modules/product-attributes}/models/product-custom-attribute.d.ts +0 -1
  14. package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.js.map +1 -0
  15. package/{dist → .medusa/server/src/modules/product-attributes}/service.d.ts +0 -1
  16. package/.medusa/server/src/modules/product-attributes/service.js.map +1 -0
  17. package/package.json +35 -23
  18. package/dist/index.d.ts.map +0 -1
  19. package/dist/index.js.map +0 -1
  20. package/dist/models/category-custom-attribute.d.ts.map +0 -1
  21. package/dist/models/category-custom-attribute.js.map +0 -1
  22. package/dist/models/product-custom-attribute.d.ts.map +0 -1
  23. package/dist/models/product-custom-attribute.js.map +0 -1
  24. package/dist/service.d.ts.map +0 -1
  25. package/dist/service.js.map +0 -1
  26. package/src/admin/lib/sdk.ts +0 -9
  27. package/src/admin/widgets/category-attribute-templates.tsx +0 -237
  28. package/src/admin/widgets/product-attribute-values.tsx +0 -186
  29. package/src/api/admin/category/[categoryId]/custom-attributes/route.ts +0 -49
  30. package/src/api/admin/product/[productId]/custom-attributes/route.ts +0 -51
  31. /package/{dist → .medusa/server/src/modules/product-attributes}/index.js +0 -0
  32. /package/{dist → .medusa/server/src/modules/product-attributes}/models/category-custom-attribute.js +0 -0
  33. /package/{dist → .medusa/server/src/modules/product-attributes}/models/product-custom-attribute.js +0 -0
  34. /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