@empty-complete-org/medusa-product-attributes 0.14.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +441 -202
- package/.medusa/server/src/admin/index.mjs +442 -203
- package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.js +1 -1
- package/.medusa/server/src/api/admin/attribute-templates/[id]/apply/route.js.map +1 -0
- package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.js +6 -6
- package/.medusa/server/src/api/admin/attribute-templates/route.js.map +1 -0
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js +1 -1
- package/.medusa/server/src/api/admin/category/[categoryId]/custom-attributes/route.js.map +1 -1
- package/.medusa/server/src/api/admin/global-attributes/route.d.ts +4 -0
- package/.medusa/server/src/api/admin/global-attributes/route.js +30 -0
- package/.medusa/server/src/api/admin/global-attributes/route.js.map +1 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.d.ts +2 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js +34 -0
- package/.medusa/server/src/api/admin/product/[productId]/attribute-schema/route.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/index.d.ts +5 -5
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.d.ts +5 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js +58 -0
- package/.medusa/server/src/modules/product-attributes/migrations/Migration20260407120000.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.d.ts → attribute-template.d.ts} +3 -3
- package/.medusa/server/src/modules/product-attributes/models/{attribute-preset.js → attribute-template.js} +3 -3
- package/.medusa/server/src/modules/product-attributes/models/attribute-template.js.map +1 -0
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.d.ts +2 -1
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js +2 -1
- package/.medusa/server/src/modules/product-attributes/models/category-custom-attribute.js.map +1 -1
- package/.medusa/server/src/modules/product-attributes/models/product-custom-attribute.d.ts +2 -1
- package/.medusa/server/src/modules/product-attributes/service.d.ts +56 -17
- package/.medusa/server/src/modules/product-attributes/service.js +44 -18
- package/.medusa/server/src/modules/product-attributes/service.js.map +1 -1
- package/README.md +96 -312
- package/README.ru.md +85 -0
- package/package.json +1 -1
- package/.medusa/server/src/api/admin/attribute-presets/[id]/apply/route.js.map +0 -1
- package/.medusa/server/src/api/admin/attribute-presets/route.js.map +0 -1
- package/.medusa/server/src/modules/product-attributes/models/attribute-preset.js.map +0 -1
- /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/[id]/apply/route.d.ts +0 -0
- /package/.medusa/server/src/api/admin/{attribute-presets → attribute-templates}/route.d.ts +0 -0
|
@@ -7,11 +7,11 @@ const utils_1 = require("@medusajs/framework/utils");
|
|
|
7
7
|
const transliteration_1 = require("transliteration");
|
|
8
8
|
const category_custom_attribute_1 = __importDefault(require("./models/category-custom-attribute"));
|
|
9
9
|
const product_custom_attribute_1 = __importDefault(require("./models/product-custom-attribute"));
|
|
10
|
-
const
|
|
10
|
+
const attribute_template_1 = __importDefault(require("./models/attribute-template"));
|
|
11
11
|
const models = {
|
|
12
12
|
CategoryCustomAttribute: category_custom_attribute_1.default,
|
|
13
13
|
ProductCustomAttribute: product_custom_attribute_1.default,
|
|
14
|
-
|
|
14
|
+
AttributeTemplate: attribute_template_1.default,
|
|
15
15
|
};
|
|
16
16
|
// @ts-ignore - MedusaService dynamically generates methods based on models
|
|
17
17
|
class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
@@ -20,25 +20,42 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
|
20
20
|
return await this.listCategoryCustomAttributes({
|
|
21
21
|
category_id: categoryId,
|
|
22
22
|
deleted_at: null,
|
|
23
|
+
is_global: false,
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
|
-
async getAttributesByCategoryIds(categoryIds) {
|
|
26
|
+
async getAttributesByCategoryIds(categoryIds, opts = { includeGlobals: true }) {
|
|
27
|
+
// Always include global attributes (applies to every product) unless disabled
|
|
28
|
+
let globalsMapped = [];
|
|
29
|
+
if (opts.includeGlobals) {
|
|
30
|
+
// @ts-ignore - method generated by MedusaService
|
|
31
|
+
const globals = await this.listCategoryCustomAttributes({
|
|
32
|
+
is_global: true,
|
|
33
|
+
deleted_at: null,
|
|
34
|
+
});
|
|
35
|
+
globalsMapped = globals.map((attr) => ({
|
|
36
|
+
...attr,
|
|
37
|
+
inherited: true,
|
|
38
|
+
source_category_id: null,
|
|
39
|
+
source: "global",
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
26
42
|
if (!categoryIds || categoryIds.length === 0) {
|
|
27
|
-
return
|
|
43
|
+
return globalsMapped;
|
|
28
44
|
}
|
|
29
45
|
// @ts-ignore - method generated by MedusaService
|
|
30
46
|
const attributes = await this.listCategoryCustomAttributes({
|
|
31
47
|
category_id: categoryIds,
|
|
48
|
+
is_global: false,
|
|
32
49
|
deleted_at: null,
|
|
33
50
|
});
|
|
34
|
-
// First id in the list is the leaf category — its attributes are "own",
|
|
35
|
-
// everything else is inherited from an ancestor.
|
|
36
51
|
const leafId = categoryIds[0];
|
|
37
|
-
|
|
52
|
+
const categoryMapped = attributes.map((attr) => ({
|
|
38
53
|
...attr,
|
|
39
54
|
inherited: attr.category_id !== leafId,
|
|
40
55
|
source_category_id: attr.category_id,
|
|
56
|
+
source: "category",
|
|
41
57
|
}));
|
|
58
|
+
return [...globalsMapped, ...categoryMapped];
|
|
42
59
|
}
|
|
43
60
|
async createCategoryAttribute(data) {
|
|
44
61
|
const key = this.generateKey(data.label);
|
|
@@ -47,7 +64,9 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
|
47
64
|
...data,
|
|
48
65
|
key,
|
|
49
66
|
unit: data.unit ?? null,
|
|
67
|
+
category_id: data.category_id ?? null,
|
|
50
68
|
is_standard: data.is_standard ?? false,
|
|
69
|
+
is_global: data.is_global ?? false,
|
|
51
70
|
sort_order: data.sort_order ?? 0,
|
|
52
71
|
});
|
|
53
72
|
}
|
|
@@ -59,6 +78,13 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
|
59
78
|
// @ts-ignore - method generated by MedusaService
|
|
60
79
|
return await this.updateCategoryCustomAttributes([{ id, ...updateData }]);
|
|
61
80
|
}
|
|
81
|
+
async listGlobalAttributes() {
|
|
82
|
+
// @ts-ignore - generated
|
|
83
|
+
return await this.listCategoryCustomAttributes({
|
|
84
|
+
is_global: true,
|
|
85
|
+
deleted_at: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
62
88
|
async createProductAttribute(data) {
|
|
63
89
|
// @ts-ignore - method generated by MedusaService
|
|
64
90
|
return await this.createProductCustomAttributes(data);
|
|
@@ -76,14 +102,14 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
|
76
102
|
relations: ["category_custom_attribute"],
|
|
77
103
|
});
|
|
78
104
|
}
|
|
79
|
-
async
|
|
105
|
+
async listTemplates() {
|
|
80
106
|
// @ts-ignore - generated
|
|
81
|
-
return await this.
|
|
107
|
+
return await this.listAttributeTemplates({ deleted_at: null });
|
|
82
108
|
}
|
|
83
|
-
async
|
|
109
|
+
async createTemplate(data) {
|
|
84
110
|
const key = this.generateKey(data.label);
|
|
85
111
|
// @ts-ignore - generated
|
|
86
|
-
return await this.
|
|
112
|
+
return await this.createAttributeTemplates({
|
|
87
113
|
label: data.label,
|
|
88
114
|
key,
|
|
89
115
|
type: data.type || "text",
|
|
@@ -91,21 +117,21 @@ class CustomAttributeService extends (0, utils_1.MedusaService)(models) {
|
|
|
91
117
|
description: data.description ?? null,
|
|
92
118
|
});
|
|
93
119
|
}
|
|
94
|
-
async
|
|
120
|
+
async updateTemplate(id, data) {
|
|
95
121
|
const updateData = { ...data };
|
|
96
122
|
if (data.label) {
|
|
97
123
|
updateData.key = this.generateKey(data.label);
|
|
98
124
|
}
|
|
99
125
|
// @ts-ignore - generated
|
|
100
|
-
return await this.
|
|
126
|
+
return await this.updateAttributeTemplates([{ id, ...updateData }]);
|
|
101
127
|
}
|
|
102
|
-
async
|
|
128
|
+
async applyTemplateToCategory(templateId, categoryId) {
|
|
103
129
|
// @ts-ignore - generated
|
|
104
|
-
const
|
|
130
|
+
const template = await this.retrieveAttributeTemplate(templateId);
|
|
105
131
|
return await this.createCategoryAttribute({
|
|
106
|
-
label:
|
|
107
|
-
type:
|
|
108
|
-
unit:
|
|
132
|
+
label: template.label,
|
|
133
|
+
type: template.type,
|
|
134
|
+
unit: template.unit,
|
|
109
135
|
category_id: categoryId,
|
|
110
136
|
is_standard: true,
|
|
111
137
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../../src/modules/product-attributes/service.ts"],"names":[],"mappings":";;;;;AAAA,qDAAyD;AACzD,qDAAyC;AACzC,mGAAwE;AACxE,iGAAsE;AACtE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../../../../src/modules/product-attributes/service.ts"],"names":[],"mappings":"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"}
|
package/README.md
CHANGED
|
@@ -1,367 +1,151 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="
|
|
3
|
-
<h1>Product Attributes
|
|
4
|
-
<p><i>
|
|
5
|
-
|
|
2
|
+
<img src="./assets/logo.png" alt="medusa-product-attributes logo" width="120">
|
|
3
|
+
<h1>Medusa Product Attributes</h1>
|
|
4
|
+
<p><i>A flexible attribute system for Medusa v2 — text, number, file, boolean, with units and category inheritance</i></p>
|
|
5
|
+
|
|
6
6
|
<p>
|
|
7
7
|
<a href="https://github.com/empty-complete/medusa-product-attributes/blob/main/LICENSE">
|
|
8
|
-
<img src="https://img.shields.io/
|
|
8
|
+
<img src="https://img.shields.io/npm/l/@empty-complete-org/medusa-product-attributes" alt="License">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@empty-complete-org/medusa-product-attributes">
|
|
11
|
+
<img src="https://img.shields.io/npm/v/@empty-complete-org/medusa-product-attributes" alt="npm version">
|
|
9
12
|
</a>
|
|
10
|
-
<a href="https://www.npmjs.com/package/@empty-complete/medusa-product-attributes">
|
|
11
|
-
<img src="https://img.shields.io/npm/
|
|
13
|
+
<a href="https://www.npmjs.com/package/@empty-complete-org/medusa-product-attributes">
|
|
14
|
+
<img src="https://img.shields.io/npm/dm/@empty-complete-org/medusa-product-attributes" alt="npm downloads">
|
|
12
15
|
</a>
|
|
13
16
|
<a href="https://github.com/empty-complete/medusa-product-attributes/actions/workflows/ci.yml">
|
|
14
17
|
<img src="https://github.com/empty-complete/medusa-product-attributes/actions/workflows/ci.yml/badge.svg" alt="CI Status">
|
|
15
18
|
</a>
|
|
16
|
-
<a href="https://github.com/empty-complete/medusa-product-attributes/commits/main">
|
|
17
|
-
<img src="https://img.shields.io/github/commit-activity/m/empty-complete/medusa-product-attributes" alt="GitHub commit activity">
|
|
18
|
-
</a>
|
|
19
|
-
<a href="https://www.npmjs.com/package/@empty-complete/medusa-product-attributes">
|
|
20
|
-
<img src="https://img.shields.io/npm/dm/@empty-complete/medusa-product-attributes" alt="npm downloads">
|
|
21
|
-
</a>
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
<p>
|
|
25
|
-
<a href="https://medusajs.com/">
|
|
26
|
-
<img src="https://img.shields.io/badge/Medusa-v2-000000" alt="Medusa v2">
|
|
27
|
-
</a>
|
|
28
|
-
<a href="https://www.typescriptlang.org/">
|
|
29
|
-
<img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6" alt="TypeScript">
|
|
30
|
-
</a>
|
|
31
|
-
<a href="https://nodejs.org/">
|
|
32
|
-
<img src="https://img.shields.io/badge/Node.js-%3E=20-339933" alt="Node.js">
|
|
33
|
-
</a>
|
|
34
19
|
</p>
|
|
35
|
-
|
|
20
|
+
|
|
36
21
|
<p>
|
|
37
|
-
<a href="https://
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
<a href="https://medusajs.com/"><img src="https://img.shields.io/badge/Medusa-v2-000000" alt="Medusa v2"></a>
|
|
23
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6" alt="TypeScript"></a>
|
|
24
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E=20-339933" alt="Node.js"></a>
|
|
40
25
|
</p>
|
|
26
|
+
|
|
27
|
+
<p><a href="./README.ru.md">Русская версия</a></p>
|
|
41
28
|
</div>
|
|
42
29
|
|
|
43
30
|
---
|
|
44
31
|
|
|
45
|
-
##
|
|
32
|
+
## Features
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
- **4 attribute types**: text, number (with units), file, boolean
|
|
35
|
+
- **Category inheritance**: attributes defined on a parent category are automatically inherited by all subcategories
|
|
36
|
+
- **Global attributes**: applied to every product in the store
|
|
37
|
+
- **Templates**: reusable attribute blueprints — apply to any category in one click
|
|
38
|
+
- **Automatic slug generation**: labels are transliterated (works with Russian, Chinese, any script)
|
|
39
|
+
- **Smart file naming**: uploaded images are renamed to `{product_handle}_{attr_key}.ext`
|
|
40
|
+
- **Admin UI included**: category widget, product widget, two Settings pages
|
|
41
|
+
- **Ships with migrations**: just run `npx medusa db:migrate`
|
|
55
42
|
|
|
56
|
-
|
|
43
|
+
## Screenshots
|
|
57
44
|
|
|
58
|
-
###
|
|
45
|
+
### Category widget with inheritance
|
|
46
|
+

|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| Medusa | ^2.0.0 |
|
|
63
|
-
| Node.js | >=20 |
|
|
64
|
-
| TypeScript | ^5.0.0 |
|
|
48
|
+
### Product attribute values
|
|
49
|
+

|
|
65
50
|
|
|
66
|
-
|
|
51
|
+
### Global attributes settings
|
|
52
|
+

|
|
67
53
|
|
|
68
|
-
|
|
54
|
+
### Attribute templates settings
|
|
55
|
+

|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
## Installation
|
|
71
58
|
|
|
72
59
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# Via yarn
|
|
80
|
-
yarn add @empty-complete/medusa-product-attributes
|
|
81
|
-
|
|
82
|
-
# Via GitHub Packages (requires authentication)
|
|
83
|
-
npm install @empty-complete/medusa-product-attributes --registry=https://npm.pkg.github.com
|
|
60
|
+
pnpm add @empty-complete-org/medusa-product-attributes
|
|
61
|
+
# or
|
|
62
|
+
npm install @empty-complete-org/medusa-product-attributes
|
|
63
|
+
# or
|
|
64
|
+
yarn add @empty-complete-org/medusa-product-attributes
|
|
84
65
|
```
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// medusa-config.ts
|
|
90
|
-
import { defineConfig } from '@medusajs/framework/utils'
|
|
67
|
+
Add to `medusa-config.ts`:
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
69
|
+
```ts
|
|
70
|
+
module.exports = defineConfig({
|
|
71
|
+
plugins: [
|
|
94
72
|
{
|
|
95
|
-
resolve:
|
|
73
|
+
resolve: "@empty-complete-org/medusa-product-attributes",
|
|
74
|
+
options: {},
|
|
96
75
|
},
|
|
97
76
|
],
|
|
98
77
|
})
|
|
99
78
|
```
|
|
100
79
|
|
|
101
|
-
|
|
80
|
+
Run migrations:
|
|
102
81
|
|
|
103
82
|
```bash
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 3. Done!
|
|
108
|
-
|
|
109
|
-
The module will automatically create the necessary tables and register services.
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## 💡 Usage
|
|
114
|
-
|
|
115
|
-
### Creating a category attribute
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import { CUSTOM_ATTRIBUTE_MODULE } from '@empty-complete/medusa-product-attributes'
|
|
119
|
-
|
|
120
|
-
const module = container.resolve(CUSTOM_ATTRIBUTE_MODULE)
|
|
121
|
-
|
|
122
|
-
// Create a "Weight" attribute with unit
|
|
123
|
-
await module.createCategoryAttribute({
|
|
124
|
-
label: 'Weight',
|
|
125
|
-
type: 'number',
|
|
126
|
-
unit: 'kg',
|
|
127
|
-
category_id: 'pcat_123',
|
|
128
|
-
sort_order: 0,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Create a 3D model attribute
|
|
132
|
-
await module.createCategoryAttribute({
|
|
133
|
-
label: '3D Model',
|
|
134
|
-
type: 'file',
|
|
135
|
-
category_id: 'pcat_123',
|
|
136
|
-
is_standard: true,
|
|
137
|
-
})
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Adding attributes to a product
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// Get the attribute template
|
|
144
|
-
const attributes = await module.getCategoryAttributes('pcat_123')
|
|
145
|
-
|
|
146
|
-
// Add values to the product
|
|
147
|
-
await module.createProductAttribute({
|
|
148
|
-
product_id: 'prod_456',
|
|
149
|
-
category_custom_attribute_id: attributes[0].id,
|
|
150
|
-
value_numeric: 5.5,
|
|
151
|
-
})
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Getting product attributes
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
const productAttributes = await module.getProductAttributes('prod_456')
|
|
158
|
-
|
|
159
|
-
console.log(productAttributes)
|
|
160
|
-
// [
|
|
161
|
-
// {
|
|
162
|
-
// id: 'pat_...',
|
|
163
|
-
// value: 'Premium quality',
|
|
164
|
-
// value_numeric: 5.5,
|
|
165
|
-
// value_file: null,
|
|
166
|
-
// category_custom_attribute: {
|
|
167
|
-
// label: 'Weight',
|
|
168
|
-
// unit: 'kg',
|
|
169
|
-
// type: 'number'
|
|
170
|
-
// }
|
|
171
|
-
// }
|
|
172
|
-
// ]
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## 📚 API Reference
|
|
178
|
-
|
|
179
|
-
### CategoryCustomAttribute
|
|
180
|
-
|
|
181
|
-
| Method | Description | Parameters |
|
|
182
|
-
|--------|-------------|------------|
|
|
183
|
-
| `getCategoryAttributes(categoryId)` | Get all category attributes | `categoryId: string` |
|
|
184
|
-
| `createCategoryAttribute(data)` | Create a new attribute | See below |
|
|
185
|
-
| `updateCategoryAttribute(id, data)` | Update an attribute | `id: string`, `data: Partial<Attribute>` |
|
|
186
|
-
|
|
187
|
-
#### createCategoryAttribute
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
{
|
|
191
|
-
label: string // Display name
|
|
192
|
-
type: 'text' | 'number' | 'file' | 'boolean'
|
|
193
|
-
unit?: string | null // Unit of measurement (optional)
|
|
194
|
-
category_id: string // Category ID
|
|
195
|
-
is_standard?: boolean // Standard attribute flag
|
|
196
|
-
sort_order?: number // Sort order
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### ProductCustomAttribute
|
|
201
|
-
|
|
202
|
-
| Method | Description | Parameters |
|
|
203
|
-
|--------|-------------|------------|
|
|
204
|
-
| `getProductAttributes(productId)` | Get product attributes | `productId: string` |
|
|
205
|
-
| `createProductAttribute(data)` | Create attribute value | See below |
|
|
206
|
-
| `updateProductAttribute(id, data)` | Update value | `id: string`, `data: Partial<Value>` |
|
|
207
|
-
|
|
208
|
-
#### createProductAttribute
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
{
|
|
212
|
-
product_id: string
|
|
213
|
-
category_custom_attribute_id: string
|
|
214
|
-
value?: string
|
|
215
|
-
value_numeric?: number | null
|
|
216
|
-
value_file?: string | null
|
|
217
|
-
}
|
|
83
|
+
npx medusa db:migrate
|
|
218
84
|
```
|
|
219
85
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
## 🗄️ Data Structure
|
|
223
|
-
|
|
224
|
-
### CategoryCustomAttribute
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
┌─────────────────────────────────────────┐
|
|
228
|
-
│ CategoryCustomAttribute │
|
|
229
|
-
├─────────────────────────────────────────┤
|
|
230
|
-
│ id: string (PK) │
|
|
231
|
-
│ key: string │
|
|
232
|
-
│ label: string │
|
|
233
|
-
│ type: 'text' | 'number' | 'file' │
|
|
234
|
-
│ unit: string | null │
|
|
235
|
-
│ sort_order: number │
|
|
236
|
-
│ is_standard: boolean │
|
|
237
|
-
│ category_id: string │
|
|
238
|
-
│ created_at: timestamp │
|
|
239
|
-
│ updated_at: timestamp │
|
|
240
|
-
│ deleted_at: timestamp | null │
|
|
241
|
-
└─────────────────────────────────────────┘
|
|
242
|
-
```
|
|
86
|
+
## Usage
|
|
243
87
|
|
|
244
|
-
###
|
|
88
|
+
### Admin
|
|
245
89
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
│ id: string (PK) │
|
|
251
|
-
│ product_id: string │
|
|
252
|
-
│ value: string | null │
|
|
253
|
-
│ value_numeric: number | null │
|
|
254
|
-
│ value_file: string | null │
|
|
255
|
-
│ category_custom_attribute_id: FK │
|
|
256
|
-
│ created_at: timestamp │
|
|
257
|
-
│ updated_at: timestamp │
|
|
258
|
-
│ deleted_at: timestamp | null │
|
|
259
|
-
└─────────────────────────────────────────┘
|
|
260
|
-
```
|
|
90
|
+
1. **Category attributes** — open any category in admin, the "Атрибуты" widget appears below the details. Add custom attributes (inherited by all subcategories), or pick one from a template.
|
|
91
|
+
2. **Global attributes** — Settings → *Global Attributes*. Applied to every product automatically.
|
|
92
|
+
3. **Templates** — Settings → *Attribute Templates*. Create reusable blueprints.
|
|
93
|
+
4. **Product values** — open any product, the "Характеристики" widget shows all inherited + global attributes with inputs for their values.
|
|
261
94
|
|
|
262
|
-
|
|
95
|
+
### API
|
|
263
96
|
|
|
264
|
-
|
|
97
|
+
```ts
|
|
98
|
+
// List all attributes for a product (globals + category chain)
|
|
99
|
+
GET /admin/product/:productId/attribute-schema
|
|
265
100
|
|
|
266
|
-
|
|
101
|
+
// List/create/update attributes for a category
|
|
102
|
+
GET /admin/category/:categoryId/custom-attributes
|
|
103
|
+
POST /admin/category/:categoryId/custom-attributes
|
|
104
|
+
PATCH /admin/category/:categoryId/custom-attributes
|
|
267
105
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
type: 'number',
|
|
273
|
-
unit: 'inch',
|
|
274
|
-
category_id: 'pcat_phones',
|
|
275
|
-
sort_order: 1,
|
|
276
|
-
})
|
|
106
|
+
// Global attributes
|
|
107
|
+
GET /admin/global-attributes
|
|
108
|
+
POST /admin/global-attributes
|
|
109
|
+
PATCH /admin/global-attributes
|
|
277
110
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
category_id
|
|
283
|
-
sort_order: 2,
|
|
284
|
-
})
|
|
111
|
+
// Templates
|
|
112
|
+
GET /admin/attribute-templates
|
|
113
|
+
POST /admin/attribute-templates
|
|
114
|
+
PATCH /admin/attribute-templates
|
|
115
|
+
POST /admin/attribute-templates/:id/apply { category_id }
|
|
285
116
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
category_id: 'pcat_phones',
|
|
290
|
-
is_standard: true,
|
|
291
|
-
sort_order: 3,
|
|
292
|
-
})
|
|
117
|
+
// Product values
|
|
118
|
+
GET /admin/product/:productId/custom-attributes
|
|
119
|
+
POST /admin/product/:productId/custom-attributes
|
|
293
120
|
```
|
|
294
121
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
// iPhone 15 Pro
|
|
299
|
-
await module.createProductAttribute({
|
|
300
|
-
product_id: 'prod_iphone15pro',
|
|
301
|
-
category_custom_attribute_id: 'attr_screen_size',
|
|
302
|
-
value_numeric: 6.1,
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
await module.createProductAttribute({
|
|
306
|
-
product_id: 'prod_iphone15pro',
|
|
307
|
-
category_custom_attribute_id: 'attr_storage',
|
|
308
|
-
value_numeric: 256,
|
|
309
|
-
})
|
|
310
|
-
```
|
|
122
|
+
## Attribute types
|
|
311
123
|
|
|
312
|
-
|
|
124
|
+
| Type | Admin input | Stored in |
|
|
125
|
+
|------|-------------|-----------|
|
|
126
|
+
| `text` | text input | `value` |
|
|
127
|
+
| `number` | number input + unit badge | `value`, `value_numeric` |
|
|
128
|
+
| `file` | upload button | `value` (URL), `value_file` |
|
|
129
|
+
| `boolean` | select Yes/No | `value` |
|
|
313
130
|
|
|
314
|
-
|
|
315
|
-
// GET /store/attribute-values?product_id=prod_iphone15pro
|
|
316
|
-
// Response:
|
|
317
|
-
{
|
|
318
|
-
"attribute_values": [
|
|
319
|
-
{
|
|
320
|
-
"id": "pat_...",
|
|
321
|
-
"name": "Screen Size",
|
|
322
|
-
"type": "number",
|
|
323
|
-
"unit": "inch",
|
|
324
|
-
"numeric_value": 6.1,
|
|
325
|
-
"is_standard": false
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"id": "pat_...",
|
|
329
|
-
"name": "Certificate",
|
|
330
|
-
"type": "file",
|
|
331
|
-
"file_value": "/files/cert_iphone15pro.pdf",
|
|
332
|
-
"is_standard": true
|
|
333
|
-
}
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
```
|
|
131
|
+
## Peer dependencies
|
|
337
132
|
|
|
338
|
-
|
|
133
|
+
- `@medusajs/framework` ^2.13
|
|
134
|
+
- `@medusajs/medusa` ^2.13
|
|
135
|
+
- `@medusajs/admin-sdk` ^2.13
|
|
136
|
+
- `@medusajs/ui` ^4
|
|
137
|
+
- `@tanstack/react-query` ^5
|
|
138
|
+
- `react` ^18 || ^19
|
|
339
139
|
|
|
340
|
-
##
|
|
140
|
+
## Development
|
|
341
141
|
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
│ │ └── product-custom-attribute.ts
|
|
348
|
-
│ ├── index.ts
|
|
349
|
-
│ └── service.ts
|
|
350
|
-
├── assets/
|
|
351
|
-
│ └── *.png
|
|
352
|
-
├── dist/
|
|
353
|
-
├── package.json
|
|
354
|
-
├── tsconfig.json
|
|
355
|
-
├── LICENSE
|
|
356
|
-
└── README.md
|
|
142
|
+
```bash
|
|
143
|
+
pnpm install
|
|
144
|
+
pnpm test
|
|
145
|
+
pnpm run build # builds to .medusa/server/
|
|
146
|
+
pnpm run dev # medusa plugin:develop
|
|
357
147
|
```
|
|
358
148
|
|
|
359
|
-
|
|
149
|
+
## License
|
|
360
150
|
|
|
361
|
-
|
|
362
|
-
<h2>🤝 Contributing</h2>
|
|
363
|
-
<p>Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.</p>
|
|
364
|
-
<p>
|
|
365
|
-
<sub>Made with ❤️ for the Medusa community</sub>
|
|
366
|
-
</p>
|
|
367
|
-
</div>
|
|
151
|
+
MIT © empty-complete
|