@empty-complete-org/medusa-product-attributes 0.10.1 → 0.11.1

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