@empty-complete-org/medusa-product-attributes 0.10.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.
@@ -0,0 +1,223 @@
1
+ declare const CustomAttributeService_base: import("@medusajs/framework/utils").MedusaServiceReturnType<import("@medusajs/framework/utils").ModelConfigurationsToConfigTemplate<{
2
+ CategoryCustomAttribute: import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder<{
3
+ id: import("@medusajs/framework/utils").PrimaryKeyModifier<string, import("@medusajs/framework/utils").IdProperty>;
4
+ key: import("@medusajs/framework/utils").TextProperty;
5
+ type: import("@medusajs/framework/utils").TextProperty;
6
+ label: import("@medusajs/framework/utils").TextProperty;
7
+ unit: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
8
+ sort_order: import("@medusajs/framework/utils").NumberProperty;
9
+ category_id: import("@medusajs/framework/utils").TextProperty;
10
+ is_standard: import("@medusajs/framework/utils").BooleanProperty;
11
+ product_custom_attributes: import("@medusajs/framework/utils").HasMany<() => import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder<{
12
+ id: import("@medusajs/framework/utils").PrimaryKeyModifier<string, import("@medusajs/framework/utils").IdProperty>;
13
+ product_id: import("@medusajs/framework/utils").TextProperty;
14
+ value: import("@medusajs/framework/utils").TextProperty;
15
+ value_numeric: import("@medusajs/framework/utils").NullableModifier<number, import("@medusajs/framework/utils").NumberProperty>;
16
+ value_file: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
17
+ category_custom_attribute: import("@medusajs/framework/utils").BelongsTo<() => import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder</*elided*/ any>, "category_custom_attribute">, undefined>;
18
+ }>, "product_custom_attribute">>;
19
+ }>, "category_custom_attribute">;
20
+ ProductCustomAttribute: import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder<{
21
+ id: import("@medusajs/framework/utils").PrimaryKeyModifier<string, import("@medusajs/framework/utils").IdProperty>;
22
+ product_id: import("@medusajs/framework/utils").TextProperty;
23
+ value: import("@medusajs/framework/utils").TextProperty;
24
+ value_numeric: import("@medusajs/framework/utils").NullableModifier<number, import("@medusajs/framework/utils").NumberProperty>;
25
+ value_file: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
26
+ category_custom_attribute: import("@medusajs/framework/utils").BelongsTo<() => import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder<{
27
+ id: import("@medusajs/framework/utils").PrimaryKeyModifier<string, import("@medusajs/framework/utils").IdProperty>;
28
+ key: import("@medusajs/framework/utils").TextProperty;
29
+ type: import("@medusajs/framework/utils").TextProperty;
30
+ label: import("@medusajs/framework/utils").TextProperty;
31
+ unit: import("@medusajs/framework/utils").NullableModifier<string, import("@medusajs/framework/utils").TextProperty>;
32
+ sort_order: import("@medusajs/framework/utils").NumberProperty;
33
+ category_id: import("@medusajs/framework/utils").TextProperty;
34
+ is_standard: import("@medusajs/framework/utils").BooleanProperty;
35
+ product_custom_attributes: import("@medusajs/framework/utils").HasMany<() => import("@medusajs/framework/utils").DmlEntity<import("@medusajs/framework/utils").DMLEntitySchemaBuilder</*elided*/ any>, "product_custom_attribute">>;
36
+ }>, "category_custom_attribute">, undefined>;
37
+ }>, "product_custom_attribute">;
38
+ }>>;
39
+ declare class CustomAttributeService extends CustomAttributeService_base {
40
+ getCategoryAttributes(categoryId: string): Promise<{
41
+ id: string;
42
+ key: string;
43
+ type: string;
44
+ label: string;
45
+ unit: string | null;
46
+ sort_order: number;
47
+ category_id: string;
48
+ is_standard: boolean;
49
+ product_custom_attributes: {
50
+ id: string;
51
+ product_id: string;
52
+ value: string;
53
+ value_numeric: number | null;
54
+ value_file: string | null;
55
+ category_custom_attribute: /*elided*/ any;
56
+ raw_value_numeric: Record<string, unknown> | null;
57
+ created_at: Date;
58
+ updated_at: Date;
59
+ deleted_at: Date | null;
60
+ category_custom_attribute_id: string;
61
+ }[];
62
+ created_at: Date;
63
+ updated_at: Date;
64
+ deleted_at: Date | null;
65
+ }[]>;
66
+ createCategoryAttribute(data: {
67
+ label: string;
68
+ type: string;
69
+ unit?: string | null;
70
+ category_id: string;
71
+ is_standard?: boolean;
72
+ sort_order?: number;
73
+ }): Promise<{
74
+ id: string;
75
+ key: string;
76
+ type: string;
77
+ label: string;
78
+ unit: string | null;
79
+ sort_order: number;
80
+ category_id: string;
81
+ is_standard: boolean;
82
+ product_custom_attributes: {
83
+ id: string;
84
+ product_id: string;
85
+ value: string;
86
+ value_numeric: number | null;
87
+ value_file: string | null;
88
+ category_custom_attribute: /*elided*/ any;
89
+ raw_value_numeric: Record<string, unknown> | null;
90
+ created_at: Date;
91
+ updated_at: Date;
92
+ deleted_at: Date | null;
93
+ category_custom_attribute_id: string;
94
+ }[];
95
+ created_at: Date;
96
+ updated_at: Date;
97
+ deleted_at: Date | null;
98
+ }>;
99
+ updateCategoryAttribute(id: string, data: {
100
+ label?: string;
101
+ type?: string;
102
+ unit?: string | null;
103
+ sort_order?: number;
104
+ deleted_at?: string;
105
+ }): Promise<{
106
+ id: string;
107
+ key: string;
108
+ type: string;
109
+ label: string;
110
+ unit: string | null;
111
+ sort_order: number;
112
+ category_id: string;
113
+ is_standard: boolean;
114
+ product_custom_attributes: {
115
+ id: string;
116
+ product_id: string;
117
+ value: string;
118
+ value_numeric: number | null;
119
+ value_file: string | null;
120
+ category_custom_attribute: /*elided*/ any;
121
+ raw_value_numeric: Record<string, unknown> | null;
122
+ created_at: Date;
123
+ updated_at: Date;
124
+ deleted_at: Date | null;
125
+ category_custom_attribute_id: string;
126
+ }[];
127
+ created_at: Date;
128
+ updated_at: Date;
129
+ deleted_at: Date | null;
130
+ }[]>;
131
+ createProductAttribute(data: {
132
+ product_id: string;
133
+ category_custom_attribute_id: string;
134
+ value: string;
135
+ value_numeric?: number | null;
136
+ value_file?: string | null;
137
+ }): Promise<{
138
+ id: string;
139
+ product_id: string;
140
+ value: string;
141
+ value_numeric: number | null;
142
+ value_file: string | null;
143
+ category_custom_attribute: {
144
+ id: string;
145
+ key: string;
146
+ type: string;
147
+ label: string;
148
+ unit: string | null;
149
+ sort_order: number;
150
+ category_id: string;
151
+ is_standard: boolean;
152
+ product_custom_attributes: /*elided*/ any[];
153
+ created_at: Date;
154
+ updated_at: Date;
155
+ deleted_at: Date | null;
156
+ };
157
+ raw_value_numeric: Record<string, unknown> | null;
158
+ created_at: Date;
159
+ updated_at: Date;
160
+ deleted_at: Date | null;
161
+ category_custom_attribute_id: string;
162
+ }>;
163
+ updateProductAttribute(id: string, data: {
164
+ value?: string;
165
+ value_numeric?: number | null;
166
+ value_file?: string | null;
167
+ deleted_at?: string;
168
+ }): Promise<{
169
+ id: string;
170
+ product_id: string;
171
+ value: string;
172
+ value_numeric: number | null;
173
+ value_file: string | null;
174
+ category_custom_attribute: {
175
+ id: string;
176
+ key: string;
177
+ type: string;
178
+ label: string;
179
+ unit: string | null;
180
+ sort_order: number;
181
+ category_id: string;
182
+ is_standard: boolean;
183
+ product_custom_attributes: /*elided*/ any[];
184
+ created_at: Date;
185
+ updated_at: Date;
186
+ deleted_at: Date | null;
187
+ };
188
+ raw_value_numeric: Record<string, unknown> | null;
189
+ created_at: Date;
190
+ updated_at: Date;
191
+ deleted_at: Date | null;
192
+ category_custom_attribute_id: string;
193
+ }[]>;
194
+ getProductAttributes(productId: string): Promise<{
195
+ id: string;
196
+ product_id: string;
197
+ value: string;
198
+ value_numeric: number | null;
199
+ value_file: string | null;
200
+ category_custom_attribute: {
201
+ id: string;
202
+ key: string;
203
+ type: string;
204
+ label: string;
205
+ unit: string | null;
206
+ sort_order: number;
207
+ category_id: string;
208
+ is_standard: boolean;
209
+ product_custom_attributes: /*elided*/ any[];
210
+ created_at: Date;
211
+ updated_at: Date;
212
+ deleted_at: Date | null;
213
+ };
214
+ raw_value_numeric: Record<string, unknown> | null;
215
+ created_at: Date;
216
+ updated_at: Date;
217
+ deleted_at: Date | null;
218
+ category_custom_attribute_id: string;
219
+ }[]>;
220
+ private generateKey;
221
+ }
222
+ export default CustomAttributeService;
223
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,cAAM,sBAAuB,SAAQ,2BAAqB;IAClD,qBAAqB,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;IAQxC,uBAAuB,CAAC,IAAI,EAAE;QAClC,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,WAAW,EAAE,MAAM,CAAA;QACnB,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB;;;;;;;;;;;;;;;;;;;;;;;;;;IAYK,uBAAuB,CAC3B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB;;;;;;;;;;;;;;;;;;;;;;;;;;IAUG,sBAAsB,CAAC,IAAI,EAAE;QACjC,UAAU,EAAE,MAAM,CAAA;QAClB,4BAA4B,EAAE,MAAM,CAAA;QACpC,KAAK,EAAE,MAAM,CAAA;QACb,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;IAKK,sBAAsB,CAC1B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB;;;;;;;;;;;;;;;;;;;;;;;;;;IAMG,oBAAoB,CAAC,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;IAU5C,OAAO,CAAC,WAAW;CAMpB;AAED,eAAe,sBAAsB,CAAA"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("@medusajs/framework/utils");
7
+ const category_custom_attribute_1 = __importDefault(require("./models/category-custom-attribute"));
8
+ const product_custom_attribute_1 = __importDefault(require("./models/product-custom-attribute"));
9
+ const models = {
10
+ CategoryCustomAttribute: category_custom_attribute_1.default,
11
+ ProductCustomAttribute: product_custom_attribute_1.default,
12
+ };
13
+ // @ts-ignore - MedusaService dynamically generates methods based on models
14
+ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
15
+ async getCategoryAttributes(categoryId) {
16
+ // @ts-ignore - method generated by MedusaService
17
+ return await this.listCategoryCustomAttributes({
18
+ category_id: categoryId,
19
+ deleted_at: null,
20
+ });
21
+ }
22
+ async createCategoryAttribute(data) {
23
+ const key = this.generateKey(data.label);
24
+ // @ts-ignore - method generated by MedusaService
25
+ return await this.createCategoryCustomAttributes({
26
+ ...data,
27
+ key,
28
+ unit: data.unit ?? null,
29
+ is_standard: data.is_standard ?? false,
30
+ sort_order: data.sort_order ?? 0,
31
+ });
32
+ }
33
+ async updateCategoryAttribute(id, data) {
34
+ const updateData = { ...data };
35
+ if (data.label) {
36
+ updateData.key = this.generateKey(data.label);
37
+ }
38
+ // @ts-ignore - method generated by MedusaService
39
+ return await this.updateCategoryCustomAttributes([{ id, ...updateData }]);
40
+ }
41
+ async createProductAttribute(data) {
42
+ // @ts-ignore - method generated by MedusaService
43
+ return await this.createProductCustomAttributes(data);
44
+ }
45
+ async updateProductAttribute(id, data) {
46
+ // @ts-ignore - method generated by MedusaService
47
+ return await this.updateProductCustomAttributes([{ id, ...data }]);
48
+ }
49
+ async getProductAttributes(productId) {
50
+ // @ts-ignore - method generated by MedusaService
51
+ return await this.listProductCustomAttributes({
52
+ product_id: productId,
53
+ deleted_at: null,
54
+ }, {
55
+ relations: ["category_custom_attribute"],
56
+ });
57
+ }
58
+ generateKey(label) {
59
+ return label
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]+/g, "_")
62
+ .replace(/^_|_$/g, "");
63
+ }
64
+ }
65
+ exports.default = CustomAttributeService;
66
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":";;;;;AAAA,qDAAyD;AACzD,mGAAwE;AACxE,iGAAsE;AAEtE,MAAM,MAAM,GAAG;IACb,uBAAuB,EAAvB,mCAAuB;IACvB,sBAAsB,EAAtB,kCAAsB;CACvB,CAAA;AAID,2EAA2E;AAC3E,MAAM,sBAAuB,SAAQ,IAAA,qBAAa,EAAC,MAAM,CAAC;IACxD,KAAK,CAAC,qBAAqB,CAAC,UAAkB;QAC5C,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,4BAA4B,CAAC;YAC7C,WAAW,EAAE,UAAU;YACvB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,IAO7B;QACC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACxC,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,8BAA8B,CAAC;YAC/C,GAAG,IAAI;YACP,GAAG;YACH,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,KAAK;YACtC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC;SACjC,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAC3B,EAAU,EACV,IAMC;QAED,MAAM,UAAU,GAAwB,EAAE,GAAG,IAAI,EAAE,CAAA;QACnD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,UAAU,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,CAAC;QACD,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,8BAA8B,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;IAC3E,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,IAM5B;QACC,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,sBAAsB,CAC1B,EAAU,EACV,IAKC;QAED,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,6BAA6B,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;IACpE,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,SAAiB;QAC1C,iDAAiD;QACjD,OAAO,MAAM,IAAI,CAAC,2BAA2B,CAAC;YAC5C,UAAU,EAAE,SAAS;YACrB,UAAU,EAAE,IAAI;SACjB,EAAE;YACD,SAAS,EAAE,CAAC,2BAA2B,CAAC;SACzC,CAAC,CAAA;IACJ,CAAC;IAEO,WAAW,CAAC,KAAa;QAC/B,OAAO,KAAK;aACT,WAAW,EAAE;aACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;aAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC1B,CAAC;CACF;AAED,kBAAe,sBAAsB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@empty-complete-org/medusa-product-attributes",
3
+ "version": "0.10.0",
4
+ "description": "Custom attributes module for Medusa v2 with support for text, number, file types and units of measurement",
5
+ "author": "empty-complete",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/empty-complete/medusa-product-attributes.git"
10
+ },
11
+ "keywords": [
12
+ "medusa",
13
+ "medusajs-v2",
14
+ "medusa-module",
15
+ "custom-attributes",
16
+ "product-attributes",
17
+ "ecommerce",
18
+ "typescript",
19
+ "product"
20
+ ],
21
+ "files": [
22
+ "dist",
23
+ "src/admin",
24
+ "src/api"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.js"
30
+ },
31
+ "./models/*": "./dist/models/*.js",
32
+ "./service": "./dist/service.js",
33
+ "./admin/*": "./src/admin/*.tsx",
34
+ "./api/*": "./src/api/*"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "clean": "rm -rf dist",
39
+ "prepublishOnly": "npm run clean && npm run build",
40
+ "test": "jest",
41
+ "test:watch": "jest --watch",
42
+ "test:coverage": "jest --coverage",
43
+ "version:inc": "node -e \"const p=require('./package.json'),v=p.version.split('.').map(Number),n=process.argv[2]||'patch';if(n==='major'){v[0]++;v[1]=0;v[2]=0}else if(n==='minor'){v[1]++;v[2]=0}else{v[2]++};p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\"",
44
+ "prepare": "husky"
45
+ },
46
+ "peerDependencies": {
47
+ "@medusajs/framework": "^2.0.0",
48
+ "@medusajs/admin-sdk": "^2.0.0",
49
+ "@medusajs/ui": "^4.0.0",
50
+ "@medusajs/js-sdk": "^2.0.0",
51
+ "@tanstack/react-query": "^5.0.0",
52
+ "react": "^18.0.0 || ^19.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@jest/globals": "^30.0.0",
56
+ "@medusajs/framework": "^2.13.1",
57
+ "@types/jest": "^30.0.0",
58
+ "@types/node": "^20.0.0",
59
+ "husky": "^9.1.7",
60
+ "jest": "^30.3.0",
61
+ "ts-jest": "^29.4.6",
62
+ "typescript": "^5.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=20"
66
+ },
67
+ "publishConfig": {
68
+ "registry": "https://registry.npmjs.org"
69
+ }
70
+ }
@@ -0,0 +1,9 @@
1
+ import Medusa from "@medusajs/js-sdk"
2
+
3
+ export const sdk = new Medusa({
4
+ baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
5
+ debug: import.meta.env.DEV,
6
+ auth: {
7
+ type: "session",
8
+ },
9
+ })
@@ -0,0 +1,237 @@
1
+ import { defineWidgetConfig } from "@medusajs/admin-sdk"
2
+ import {
3
+ DetailWidgetProps,
4
+ AdminProductCategory,
5
+ } from "@medusajs/framework/types"
6
+ import { Container, Heading, Button, Input, Text, Badge } from "@medusajs/ui"
7
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
8
+ import { useState } from "react"
9
+ import { sdk } from "../lib/sdk"
10
+
11
+ type CategoryCustomAttribute = {
12
+ id: string
13
+ label: string
14
+ type: "text" | "number" | "file" | "boolean"
15
+ category_id: string
16
+ }
17
+
18
+ type FormState = {
19
+ label: string
20
+ type: "text" | "number"
21
+ }
22
+
23
+ const emptyForm = (): FormState => ({ label: "", type: "text" })
24
+
25
+ const CategoryAttributeTemplatesWidget = ({
26
+ data,
27
+ }: DetailWidgetProps<AdminProductCategory>) => {
28
+ const categoryId = data.id
29
+ const qc = useQueryClient()
30
+ const queryKey = ["category-custom-attributes", categoryId]
31
+
32
+ const [showAddForm, setShowAddForm] = useState(false)
33
+ const [addForm, setAddForm] = useState<FormState>(emptyForm())
34
+ const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
35
+ const [mutationError, setMutationError] = useState<string | null>(null)
36
+
37
+ const {
38
+ data: result,
39
+ isLoading,
40
+ isError,
41
+ } = useQuery<{
42
+ category_custom_attributes: CategoryCustomAttribute[]
43
+ }>({
44
+ queryKey,
45
+ queryFn: () =>
46
+ sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`),
47
+ })
48
+
49
+ const attributes = result?.category_custom_attributes ?? []
50
+
51
+ const createMutation = useMutation({
52
+ mutationFn: (body: { label: string; type: string }) =>
53
+ sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
54
+ method: "POST",
55
+ body,
56
+ }),
57
+ onSuccess: () => {
58
+ qc.invalidateQueries({ queryKey })
59
+ setShowAddForm(false)
60
+ setAddForm(emptyForm())
61
+ setMutationError(null)
62
+ },
63
+ onError: (err: any) => {
64
+ setMutationError(err?.message || "Ошибка при создании атрибута")
65
+ },
66
+ })
67
+
68
+ const deleteMutation = useMutation({
69
+ mutationFn: (id: string) =>
70
+ sdk.client.fetch(`/admin/category/${categoryId}/custom-attributes`, {
71
+ method: "PATCH",
72
+ body: { id, deleted_at: new Date().toISOString() },
73
+ }),
74
+ onSuccess: () => {
75
+ qc.invalidateQueries({ queryKey })
76
+ setConfirmDeleteId(null)
77
+ },
78
+ })
79
+
80
+ const handleAdd = () => {
81
+ if (!addForm.label.trim()) return
82
+ createMutation.mutate({
83
+ label: addForm.label.trim(),
84
+ type: addForm.type,
85
+ })
86
+ }
87
+
88
+ return (
89
+ <Container className="divide-y p-0">
90
+ <div className="flex items-center justify-between px-6 py-4">
91
+ <Heading level="h2">Атрибуты</Heading>
92
+ {!showAddForm && (
93
+ <Button
94
+ variant="secondary"
95
+ size="small"
96
+ onClick={() => setShowAddForm(true)}
97
+ >
98
+ + Добавить
99
+ </Button>
100
+ )}
101
+ </div>
102
+
103
+ {isLoading && (
104
+ <div className="px-6 py-4">
105
+ <Text className="text-ui-fg-muted text-sm">Загрузка…</Text>
106
+ </div>
107
+ )}
108
+
109
+ {isError && (
110
+ <div className="px-6 py-4">
111
+ <Text className="text-ui-fg-error text-sm">
112
+ Не удалось загрузить атрибуты.
113
+ </Text>
114
+ </div>
115
+ )}
116
+
117
+ {!isLoading && !isError && attributes.length === 0 && !showAddForm && (
118
+ <div className="px-6 py-4">
119
+ <Text className="text-ui-fg-muted text-sm">
120
+ Нет атрибутов. Добавьте первый.
121
+ </Text>
122
+ </div>
123
+ )}
124
+
125
+ {attributes.length > 0 && (
126
+ <div className="divide-y">
127
+ {attributes.map((attr) =>
128
+ confirmDeleteId === attr.id ? (
129
+ <div
130
+ key={attr.id}
131
+ className="flex items-center gap-3 px-6 py-3 text-sm"
132
+ >
133
+ <span className="flex-1 text-ui-fg-base">
134
+ Удалить «{attr.label}»?
135
+ </span>
136
+ <Button
137
+ size="small"
138
+ variant="danger"
139
+ onClick={() => deleteMutation.mutate(attr.id)}
140
+ isLoading={deleteMutation.isPending}
141
+ >
142
+ Удалить
143
+ </Button>
144
+ <Button
145
+ size="small"
146
+ variant="secondary"
147
+ onClick={() => setConfirmDeleteId(null)}
148
+ >
149
+ Отмена
150
+ </Button>
151
+ </div>
152
+ ) : (
153
+ <div
154
+ key={attr.id}
155
+ className="flex items-center gap-3 px-6 py-3"
156
+ >
157
+ <span className="flex-1 text-sm text-ui-fg-base">
158
+ {attr.label}
159
+ </span>
160
+ <Badge size="2xsmall" color="grey">
161
+ {attr.type === "text"
162
+ ? "Текст"
163
+ : attr.type === "number"
164
+ ? "Число"
165
+ : attr.type}
166
+ </Badge>
167
+ <button
168
+ onClick={() => setConfirmDeleteId(attr.id)}
169
+ className="text-xs text-ui-fg-error hover:underline"
170
+ >
171
+ Удалить
172
+ </button>
173
+ </div>
174
+ )
175
+ )}
176
+ </div>
177
+ )}
178
+
179
+ {showAddForm && (
180
+ <div className="flex items-center gap-2 px-6 py-3">
181
+ <Input
182
+ value={addForm.label}
183
+ onChange={(e) =>
184
+ setAddForm((f) => ({ ...f, label: e.target.value }))
185
+ }
186
+ placeholder="Название атрибута"
187
+ className="flex-1 h-8 text-sm"
188
+ autoFocus
189
+ />
190
+ <select
191
+ value={addForm.type}
192
+ onChange={(e) =>
193
+ setAddForm((f) => ({
194
+ ...f,
195
+ type: e.target.value as "text" | "number",
196
+ }))
197
+ }
198
+ className="h-8 rounded border border-ui-border-base bg-ui-bg-base px-2 text-sm"
199
+ >
200
+ <option value="text">Текст</option>
201
+ <option value="number">Число</option>
202
+ </select>
203
+ <Button
204
+ size="small"
205
+ onClick={handleAdd}
206
+ isLoading={createMutation.isPending}
207
+ >
208
+ Добавить
209
+ </Button>
210
+ <Button
211
+ variant="secondary"
212
+ size="small"
213
+ onClick={() => {
214
+ setShowAddForm(false)
215
+ setAddForm(emptyForm())
216
+ setMutationError(null)
217
+ }}
218
+ >
219
+ Отмена
220
+ </Button>
221
+ </div>
222
+ )}
223
+
224
+ {mutationError && (
225
+ <div className="px-6 py-2">
226
+ <Text className="text-ui-fg-error text-sm">{mutationError}</Text>
227
+ </div>
228
+ )}
229
+ </Container>
230
+ )
231
+ }
232
+
233
+ export const config = defineWidgetConfig({
234
+ zone: "product_category.details.after",
235
+ })
236
+
237
+ export default CategoryAttributeTemplatesWidget