@lodashventure/medusa-product-content 1.1.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/README.md +202 -0
- package/dist/admin/hooks/useProductContent.d.ts +25 -0
- package/dist/admin/hooks/useProductContent.js +165 -0
- package/dist/admin/lib/sdk.d.ts +2 -0
- package/dist/admin/lib/sdk.js +15 -0
- package/dist/admin/routes/product-content/page.d.ts +8 -0
- package/dist/admin/routes/product-content/page.js +158 -0
- package/dist/admin/utils/import-export.d.ts +30 -0
- package/dist/admin/utils/import-export.js +384 -0
- package/dist/admin/utils/locale.d.ts +13 -0
- package/dist/admin/utils/locale.js +95 -0
- package/dist/admin/utils/sanitize.d.ts +29 -0
- package/dist/admin/utils/sanitize.js +188 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/modules/product-content/index.d.ts +21 -0
- package/dist/modules/product-content/index.js +12 -0
- package/dist/modules/product-content/models/product-content.d.ts +7 -0
- package/dist/modules/product-content/models/product-content.js +18 -0
- package/dist/modules/product-content/service.d.ts +12 -0
- package/dist/modules/product-content/service.js +9 -0
- package/dist/types/index.d.ts +173 -0
- package/dist/types/index.js +5 -0
- package/package.json +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Medusa Product Content Plugin
|
|
2
|
+
|
|
3
|
+
Enhanced product content management with rich text editor and i18n specifications for Medusa Admin.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Rich Text Editor**: TipTap-based editor for long product descriptions
|
|
8
|
+
- **i18n Specifications**: Multilingual key-value product specifications
|
|
9
|
+
- **Version History**: Automatic versioning of content changes
|
|
10
|
+
- **Import/Export**: CSV/JSON import and export for specifications
|
|
11
|
+
- **SEO-Friendly**: HTML sanitization and auto-excerpt generation
|
|
12
|
+
- **Drag & Drop**: Reorder specification groups and items
|
|
13
|
+
- **Locale Support**: Full internationalization support
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @medusajs/admin-product-content
|
|
19
|
+
# or
|
|
20
|
+
yarn add @medusajs/admin-product-content
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Add the plugin to your `medusa-config.js`:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
module.exports = {
|
|
29
|
+
// ... other config
|
|
30
|
+
plugins: [
|
|
31
|
+
{
|
|
32
|
+
resolve: '@medusajs/admin-product-content',
|
|
33
|
+
options: {
|
|
34
|
+
locales: [
|
|
35
|
+
{ code: 'th-TH', label: 'ไทย', isDefault: true },
|
|
36
|
+
{ code: 'en-US', label: 'English' }
|
|
37
|
+
],
|
|
38
|
+
defaultLocale: 'th-TH',
|
|
39
|
+
versioning: {
|
|
40
|
+
enabled: true,
|
|
41
|
+
maxVersions: 10
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Admin Interface
|
|
52
|
+
|
|
53
|
+
1. Navigate to any product in Medusa Admin
|
|
54
|
+
2. Find the "Product Content" widget on the product detail page
|
|
55
|
+
3. Click "Manage Content" to open the content editor
|
|
56
|
+
4. Use the tabs to manage:
|
|
57
|
+
- **Overview**: View product information and content status
|
|
58
|
+
- **Long Description**: Edit rich text content
|
|
59
|
+
- **Specifications**: Manage i18n product specs
|
|
60
|
+
- **Versions**: View and restore content history
|
|
61
|
+
|
|
62
|
+
### Storefront Integration
|
|
63
|
+
|
|
64
|
+
Fetch product content via the Store API:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// Fetch product with metadata
|
|
68
|
+
const product = await fetch('/store/products/prod_123?expand=metadata')
|
|
69
|
+
.then(res => res.json())
|
|
70
|
+
|
|
71
|
+
// Access long description
|
|
72
|
+
const longDescription = product.metadata?.long_description
|
|
73
|
+
|
|
74
|
+
// Access specifications
|
|
75
|
+
const specs = product.metadata?.specs
|
|
76
|
+
|
|
77
|
+
// Helper function for locale fallback
|
|
78
|
+
function getSpecValue(item, locale, fallbackLocale) {
|
|
79
|
+
return item.i18n[locale]?.value ||
|
|
80
|
+
item.i18n[fallbackLocale]?.value ||
|
|
81
|
+
''
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Data Structure
|
|
86
|
+
|
|
87
|
+
### Long Description
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"metadata": {
|
|
92
|
+
"long_description": {
|
|
93
|
+
"html": "<h1>Product Title</h1><p>...</p>",
|
|
94
|
+
"richjson": { /* TipTap JSON */ },
|
|
95
|
+
"locale": "th-TH",
|
|
96
|
+
"updated_at": "2025-01-01T00:00:00Z",
|
|
97
|
+
"updated_by": "user@example.com"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Specifications
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"metadata": {
|
|
108
|
+
"specs": {
|
|
109
|
+
"default_locale": "th-TH",
|
|
110
|
+
"groups": [
|
|
111
|
+
{
|
|
112
|
+
"key": "electrical",
|
|
113
|
+
"position": 1,
|
|
114
|
+
"i18n": {
|
|
115
|
+
"th-TH": { "label": "ข้อมูลไฟฟ้า" },
|
|
116
|
+
"en-US": { "label": "Electrical" }
|
|
117
|
+
},
|
|
118
|
+
"items": [
|
|
119
|
+
{
|
|
120
|
+
"position": 1,
|
|
121
|
+
"i18n": {
|
|
122
|
+
"th-TH": { "key": "กำลังไฟ", "value": "18 วัตต์" },
|
|
123
|
+
"en-US": { "key": "Power", "value": "18 W" }
|
|
124
|
+
},
|
|
125
|
+
"visible": true
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Import/Export
|
|
136
|
+
|
|
137
|
+
### CSV Format
|
|
138
|
+
|
|
139
|
+
```csv
|
|
140
|
+
group_key,group_position,group_label.th-TH,group_label.en-US,item_position,key.th-TH,value.th-TH,key.en-US,value.en-US,visible
|
|
141
|
+
electrical,1,ข้อมูลไฟฟ้า,Electrical,1,กำลังไฟ,18 วัตต์,Power,18 W,true
|
|
142
|
+
electrical,1,ข้อมูลไฟฟ้า,Electrical,2,ความสว่าง,1800 ลูเมน,Brightness,1800 lm,true
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### JSON Format
|
|
146
|
+
|
|
147
|
+
Export and import full specification structure as JSON for complete data preservation.
|
|
148
|
+
|
|
149
|
+
## API
|
|
150
|
+
|
|
151
|
+
### Hooks
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { useProductContent } from '@medusajs/admin-product-content'
|
|
155
|
+
|
|
156
|
+
const {
|
|
157
|
+
product,
|
|
158
|
+
longDescription,
|
|
159
|
+
specs,
|
|
160
|
+
versions,
|
|
161
|
+
updateLongDescription,
|
|
162
|
+
updateSpecs,
|
|
163
|
+
restoreVersion
|
|
164
|
+
} = useProductContent({ productId: 'prod_123' })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Utilities
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import {
|
|
171
|
+
sanitizeHTML,
|
|
172
|
+
generateExcerpt,
|
|
173
|
+
localeUtils,
|
|
174
|
+
getLocalizedSpec
|
|
175
|
+
} from '@medusajs/admin-product-content'
|
|
176
|
+
|
|
177
|
+
// Sanitize HTML
|
|
178
|
+
const clean = sanitizeHTML(dirtyHtml)
|
|
179
|
+
|
|
180
|
+
// Generate excerpt
|
|
181
|
+
const excerpt = generateExcerpt(html, 160)
|
|
182
|
+
|
|
183
|
+
// Get localized value
|
|
184
|
+
const value = getLocalizedSpec(item, 'en-US', 'th-TH', 'value')
|
|
185
|
+
|
|
186
|
+
// Calculate completeness
|
|
187
|
+
const completeness = localeUtils.calculateLocaleCompleteness(specs, 'en-US')
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Security
|
|
191
|
+
|
|
192
|
+
- All HTML content is sanitized using DOMPurify
|
|
193
|
+
- Scripts and dangerous attributes are removed
|
|
194
|
+
- Only admin users with `product:update` permission can edit content
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
199
|
+
|
|
200
|
+
## Support
|
|
201
|
+
|
|
202
|
+
For issues and feature requests, please visit the [GitHub repository](https://github.com/medusajs/admin-product-content).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook for product content management
|
|
3
|
+
*/
|
|
4
|
+
import { ProductContentMetadata, LongDescription, LongDescriptionVersion, ProductSpecs } from "../../types";
|
|
5
|
+
export interface UseProductContentOptions {
|
|
6
|
+
productId: string;
|
|
7
|
+
maxVersions?: number;
|
|
8
|
+
autoSanitize?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UseProductContentReturn {
|
|
11
|
+
product: any;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
metadata: ProductContentMetadata;
|
|
15
|
+
longDescription: LongDescription | null;
|
|
16
|
+
specs: ProductSpecs | null;
|
|
17
|
+
versions: LongDescriptionVersion[];
|
|
18
|
+
updateLongDescription: (html: string, richjson?: any, locale?: string) => Promise<void>;
|
|
19
|
+
updateSpecs: (specs: ProductSpecs) => Promise<void>;
|
|
20
|
+
createVersion: (description: LongDescription) => Promise<void>;
|
|
21
|
+
restoreVersion: (version: number) => Promise<void>;
|
|
22
|
+
deleteVersion: (version: number) => Promise<void>;
|
|
23
|
+
updateMetadata: (updates: Partial<ProductContentMetadata>) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export declare function useProductContent(options: UseProductContentOptions): UseProductContentReturn;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Custom hook for product content management
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useProductContent = useProductContent;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_query_1 = require("@tanstack/react-query");
|
|
9
|
+
const sdk_1 = require("../lib/sdk");
|
|
10
|
+
const sanitize_1 = require("../utils/sanitize");
|
|
11
|
+
function useProductContent(options) {
|
|
12
|
+
const { productId, maxVersions = 10, autoSanitize = true } = options;
|
|
13
|
+
// Fetch product with expanded metadata
|
|
14
|
+
const { data: product, isLoading, error, } = (0, react_query_1.useQuery)({
|
|
15
|
+
queryKey: ["product", productId],
|
|
16
|
+
queryFn: async () => {
|
|
17
|
+
const response = await sdk_1.sdk.admin.product.retrieve(productId, {
|
|
18
|
+
fields: "*variants,*variants.prices,*variants.options,*options,*images,*collection,*categories,*metadata,*tags",
|
|
19
|
+
});
|
|
20
|
+
return response.product;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
// Update mutation
|
|
24
|
+
const updateProduct = (0, react_query_1.useMutation)({
|
|
25
|
+
mutationFn: async (data) => {
|
|
26
|
+
return await sdk_1.sdk.admin.product.update(productId, data);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Extract metadata
|
|
30
|
+
const metadata = (0, react_1.useMemo)(() => {
|
|
31
|
+
if (!product || !product.metadata)
|
|
32
|
+
return {};
|
|
33
|
+
return product.metadata;
|
|
34
|
+
}, [product]);
|
|
35
|
+
const longDescription = (0, react_1.useMemo)(() => {
|
|
36
|
+
return metadata.long_description || null;
|
|
37
|
+
}, [metadata.long_description]);
|
|
38
|
+
const specs = (0, react_1.useMemo)(() => {
|
|
39
|
+
return metadata.specs || null;
|
|
40
|
+
}, [metadata.specs]);
|
|
41
|
+
const versions = (0, react_1.useMemo)(() => {
|
|
42
|
+
return metadata.long_description_versions || [];
|
|
43
|
+
}, [metadata.long_description_versions]);
|
|
44
|
+
// Update long description
|
|
45
|
+
const updateLongDescription = (0, react_1.useCallback)(async (html, richjson, locale = "th-TH") => {
|
|
46
|
+
const sanitizedHtml = autoSanitize ? (0, sanitize_1.sanitizeHTML)(html) : html;
|
|
47
|
+
const newDescription = {
|
|
48
|
+
html: sanitizedHtml,
|
|
49
|
+
richjson,
|
|
50
|
+
locale,
|
|
51
|
+
updated_at: new Date().toISOString(),
|
|
52
|
+
updated_by: "current_user", // TODO: Get from auth context
|
|
53
|
+
};
|
|
54
|
+
// Create version if exists
|
|
55
|
+
let newVersions = [...versions];
|
|
56
|
+
if (longDescription) {
|
|
57
|
+
const version = {
|
|
58
|
+
...longDescription,
|
|
59
|
+
version: versions.length + 1,
|
|
60
|
+
created_at: longDescription.updated_at || new Date().toISOString(),
|
|
61
|
+
};
|
|
62
|
+
newVersions = [version, ...versions].slice(0, maxVersions);
|
|
63
|
+
}
|
|
64
|
+
await updateProduct.mutateAsync({
|
|
65
|
+
metadata: {
|
|
66
|
+
...metadata,
|
|
67
|
+
long_description: newDescription,
|
|
68
|
+
long_description_versions: newVersions,
|
|
69
|
+
content_last_updated: new Date().toISOString(),
|
|
70
|
+
content_updated_by: "current_user",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}, [
|
|
74
|
+
autoSanitize,
|
|
75
|
+
longDescription,
|
|
76
|
+
maxVersions,
|
|
77
|
+
metadata,
|
|
78
|
+
updateProduct,
|
|
79
|
+
versions,
|
|
80
|
+
]);
|
|
81
|
+
// Update specifications
|
|
82
|
+
const updateSpecs = (0, react_1.useCallback)(async (newSpecs) => {
|
|
83
|
+
await updateProduct.mutateAsync({
|
|
84
|
+
metadata: {
|
|
85
|
+
...metadata,
|
|
86
|
+
specs: newSpecs,
|
|
87
|
+
content_last_updated: new Date().toISOString(),
|
|
88
|
+
content_updated_by: "current_user",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}, [metadata, updateProduct]);
|
|
92
|
+
// Create version
|
|
93
|
+
const createVersion = (0, react_1.useCallback)(async (description) => {
|
|
94
|
+
const version = {
|
|
95
|
+
...description,
|
|
96
|
+
version: versions.length + 1,
|
|
97
|
+
created_at: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
const newVersions = [version, ...versions].slice(0, maxVersions);
|
|
100
|
+
await updateProduct.mutateAsync({
|
|
101
|
+
metadata: {
|
|
102
|
+
...metadata,
|
|
103
|
+
long_description_versions: newVersions,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}, [maxVersions, metadata, updateProduct, versions]);
|
|
107
|
+
// Restore version
|
|
108
|
+
const restoreVersion = (0, react_1.useCallback)(async (versionNumber) => {
|
|
109
|
+
const version = versions.find((v) => v.version === versionNumber);
|
|
110
|
+
if (!version) {
|
|
111
|
+
throw new Error(`Version ${versionNumber} not found`);
|
|
112
|
+
}
|
|
113
|
+
const restoredDescription = {
|
|
114
|
+
html: version.html,
|
|
115
|
+
richjson: version.richjson,
|
|
116
|
+
locale: version.locale,
|
|
117
|
+
updated_at: new Date().toISOString(),
|
|
118
|
+
updated_by: "current_user",
|
|
119
|
+
};
|
|
120
|
+
await updateProduct.mutateAsync({
|
|
121
|
+
metadata: {
|
|
122
|
+
...metadata,
|
|
123
|
+
long_description: restoredDescription,
|
|
124
|
+
content_last_updated: new Date().toISOString(),
|
|
125
|
+
content_updated_by: "current_user",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}, [metadata, updateProduct, versions]);
|
|
129
|
+
// Delete version
|
|
130
|
+
const deleteVersion = (0, react_1.useCallback)(async (versionNumber) => {
|
|
131
|
+
const newVersions = versions.filter((v) => v.version !== versionNumber);
|
|
132
|
+
await updateProduct.mutateAsync({
|
|
133
|
+
metadata: {
|
|
134
|
+
...metadata,
|
|
135
|
+
long_description_versions: newVersions,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}, [metadata, updateProduct, versions]);
|
|
139
|
+
// Update metadata
|
|
140
|
+
const updateMetadata = (0, react_1.useCallback)(async (updates) => {
|
|
141
|
+
await updateProduct.mutateAsync({
|
|
142
|
+
metadata: {
|
|
143
|
+
...metadata,
|
|
144
|
+
...updates,
|
|
145
|
+
content_last_updated: new Date().toISOString(),
|
|
146
|
+
content_updated_by: "current_user",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}, [metadata, updateProduct]);
|
|
150
|
+
return {
|
|
151
|
+
product,
|
|
152
|
+
isLoading,
|
|
153
|
+
error,
|
|
154
|
+
metadata,
|
|
155
|
+
longDescription,
|
|
156
|
+
specs,
|
|
157
|
+
versions,
|
|
158
|
+
updateLongDescription,
|
|
159
|
+
updateSpecs,
|
|
160
|
+
createVersion,
|
|
161
|
+
restoreVersion,
|
|
162
|
+
deleteVersion,
|
|
163
|
+
updateMetadata,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
exports.sdk = void 0;
|
|
7
|
+
// @ts-nocheck
|
|
8
|
+
const js_sdk_1 = __importDefault(require("@medusajs/js-sdk"));
|
|
9
|
+
exports.sdk = new js_sdk_1.default({
|
|
10
|
+
baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
|
|
11
|
+
debug: import.meta.env.DEV,
|
|
12
|
+
auth: {
|
|
13
|
+
type: "session",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Content Management Page
|
|
3
|
+
* Main listing page accessible from admin menu
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
declare const ProductContentPage: () => React.JSX.Element;
|
|
7
|
+
export declare const config: import("@medusajs/admin-sdk").RouteConfig;
|
|
8
|
+
export default ProductContentPage;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.config = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Product Content Management Page
|
|
39
|
+
* Main listing page accessible from admin menu
|
|
40
|
+
*/
|
|
41
|
+
// @ts-nocheck
|
|
42
|
+
const react_1 = __importStar(require("react"));
|
|
43
|
+
const ui_1 = require("@medusajs/ui");
|
|
44
|
+
const icons_1 = require("@medusajs/icons");
|
|
45
|
+
const admin_sdk_1 = require("@medusajs/admin-sdk");
|
|
46
|
+
const sdk_1 = require("../../lib/sdk");
|
|
47
|
+
const ProductContentPage = () => {
|
|
48
|
+
const [products, setProducts] = (0, react_1.useState)([]);
|
|
49
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
50
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
51
|
+
const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
|
|
52
|
+
(0, react_1.useEffect)(() => {
|
|
53
|
+
fetchProducts();
|
|
54
|
+
}, []);
|
|
55
|
+
const fetchProducts = async () => {
|
|
56
|
+
setLoading(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
try {
|
|
59
|
+
const response = await sdk_1.sdk.client.fetch("/admin/products?limit=100", {
|
|
60
|
+
credentials: "include",
|
|
61
|
+
});
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
setProducts(data.products || []);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
setError("Failed to fetch products");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
setError("Error fetching products");
|
|
72
|
+
console.error("Error fetching products:", err);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const handleManageContent = (productId) => {
|
|
79
|
+
window.location.href = `/app/products/${productId}`;
|
|
80
|
+
};
|
|
81
|
+
const filteredProducts = products.filter((product) => {
|
|
82
|
+
if (!searchQuery)
|
|
83
|
+
return true;
|
|
84
|
+
const query = searchQuery.toLowerCase();
|
|
85
|
+
return (product.title?.toLowerCase().includes(query) ||
|
|
86
|
+
product.handle?.toLowerCase().includes(query));
|
|
87
|
+
});
|
|
88
|
+
const hasContent = (product) => {
|
|
89
|
+
return product.metadata?.long_description || product.metadata?.specs;
|
|
90
|
+
};
|
|
91
|
+
const getContentStatus = (product) => {
|
|
92
|
+
const hasLongDesc = !!product.metadata?.long_description;
|
|
93
|
+
const hasSpecs = !!product.metadata?.specs;
|
|
94
|
+
if (hasLongDesc && hasSpecs)
|
|
95
|
+
return { label: "Complete", color: "green" };
|
|
96
|
+
if (hasLongDesc || hasSpecs)
|
|
97
|
+
return { label: "Partial", color: "orange" };
|
|
98
|
+
return { label: "Empty", color: "grey" };
|
|
99
|
+
};
|
|
100
|
+
if (loading) {
|
|
101
|
+
return (react_1.default.createElement(ui_1.Container, null,
|
|
102
|
+
react_1.default.createElement("div", { className: "flex h-64 items-center justify-center" },
|
|
103
|
+
react_1.default.createElement(ui_1.Text, null, "Loading products..."))));
|
|
104
|
+
}
|
|
105
|
+
if (error) {
|
|
106
|
+
return (react_1.default.createElement(ui_1.Container, null,
|
|
107
|
+
react_1.default.createElement(ui_1.Alert, { variant: "error", dismissible: true }, error)));
|
|
108
|
+
}
|
|
109
|
+
return (react_1.default.createElement(ui_1.Container, null,
|
|
110
|
+
react_1.default.createElement("div", { className: "mb-8" },
|
|
111
|
+
react_1.default.createElement("div", { className: "flex items-center justify-between mb-4" },
|
|
112
|
+
react_1.default.createElement("div", null,
|
|
113
|
+
react_1.default.createElement(ui_1.Heading, { level: "h1", className: "mb-2" }, "Product Content"),
|
|
114
|
+
react_1.default.createElement(ui_1.Text, { className: "text-ui-fg-subtle" }, "Manage long descriptions and specifications for all products"))),
|
|
115
|
+
react_1.default.createElement("div", { className: "flex items-center gap-4" },
|
|
116
|
+
react_1.default.createElement("div", { className: "relative flex-1 max-w-md" },
|
|
117
|
+
react_1.default.createElement(ui_1.Input, { placeholder: "Search products...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "pl-10" }),
|
|
118
|
+
react_1.default.createElement(icons_1.MagnifyingGlass, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-ui-fg-muted" })))),
|
|
119
|
+
filteredProducts.length === 0 ? (react_1.default.createElement("div", { className: "flex h-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-ui-border-base" },
|
|
120
|
+
react_1.default.createElement(icons_1.SquareTwoStack, { className: "mb-4 text-ui-fg-subtle", style: { width: "3rem", height: "3rem" } }),
|
|
121
|
+
react_1.default.createElement(ui_1.Text, { className: "mb-2 text-lg font-medium" }, searchQuery ? "No products found" : "No products yet"),
|
|
122
|
+
react_1.default.createElement(ui_1.Text, { className: "mb-4 text-ui-fg-subtle" }, searchQuery
|
|
123
|
+
? "Try adjusting your search query"
|
|
124
|
+
: "Create products to manage their content"))) : (react_1.default.createElement("div", { className: "overflow-hidden rounded-lg border" },
|
|
125
|
+
react_1.default.createElement(ui_1.Table, null,
|
|
126
|
+
react_1.default.createElement(ui_1.Table.Header, null,
|
|
127
|
+
react_1.default.createElement(ui_1.Table.Row, null,
|
|
128
|
+
react_1.default.createElement(ui_1.Table.HeaderCell, null, "Product"),
|
|
129
|
+
react_1.default.createElement(ui_1.Table.HeaderCell, null, "Handle"),
|
|
130
|
+
react_1.default.createElement(ui_1.Table.HeaderCell, null, "Status"),
|
|
131
|
+
react_1.default.createElement(ui_1.Table.HeaderCell, null, "Content Status"),
|
|
132
|
+
react_1.default.createElement(ui_1.Table.HeaderCell, { className: "text-right" }, "Actions"))),
|
|
133
|
+
react_1.default.createElement(ui_1.Table.Body, null, filteredProducts.map((product) => {
|
|
134
|
+
const contentStatus = getContentStatus(product);
|
|
135
|
+
return (react_1.default.createElement(ui_1.Table.Row, { key: product.id },
|
|
136
|
+
react_1.default.createElement(ui_1.Table.Cell, null,
|
|
137
|
+
react_1.default.createElement(ui_1.Text, { className: "font-medium" }, product.title)),
|
|
138
|
+
react_1.default.createElement(ui_1.Table.Cell, null,
|
|
139
|
+
react_1.default.createElement(ui_1.Badge, { color: "blue" }, product.handle || "—")),
|
|
140
|
+
react_1.default.createElement(ui_1.Table.Cell, null,
|
|
141
|
+
react_1.default.createElement(ui_1.Badge, { color: product.status === "published" ? "green" : "grey" }, product.status || "draft")),
|
|
142
|
+
react_1.default.createElement(ui_1.Table.Cell, null,
|
|
143
|
+
react_1.default.createElement("div", { className: "flex items-center gap-2" },
|
|
144
|
+
react_1.default.createElement(ui_1.Badge, { color: contentStatus.color }, contentStatus.label),
|
|
145
|
+
product.metadata?.long_description && (react_1.default.createElement(ui_1.Badge, { color: "green", size: "xsmall" }, "Description")),
|
|
146
|
+
product.metadata?.specs && (react_1.default.createElement(ui_1.Badge, { color: "green", size: "xsmall" }, "Specs")))),
|
|
147
|
+
react_1.default.createElement(ui_1.Table.Cell, null,
|
|
148
|
+
react_1.default.createElement("div", { className: "flex items-center justify-end" },
|
|
149
|
+
react_1.default.createElement(ui_1.Button, { variant: "secondary", size: "small", onClick: () => handleManageContent(product.id) },
|
|
150
|
+
react_1.default.createElement(icons_1.PencilSquare, { className: "mr-1" }),
|
|
151
|
+
"View Product")))));
|
|
152
|
+
})))))));
|
|
153
|
+
};
|
|
154
|
+
exports.config = (0, admin_sdk_1.defineRouteConfig)({
|
|
155
|
+
label: "Product Content",
|
|
156
|
+
icon: icons_1.SquareTwoStack,
|
|
157
|
+
});
|
|
158
|
+
exports.default = ProductContentPage;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/Export utility for specifications
|
|
3
|
+
*/
|
|
4
|
+
import { ProductSpecs, ImportValidationResult } from '../../types';
|
|
5
|
+
/**
|
|
6
|
+
* Export specifications to CSV
|
|
7
|
+
*/
|
|
8
|
+
export declare function exportSpecsToCSV(specs: ProductSpecs): string;
|
|
9
|
+
/**
|
|
10
|
+
* Export specifications to JSON
|
|
11
|
+
*/
|
|
12
|
+
export declare function exportSpecsToJSON(specs: ProductSpecs): string;
|
|
13
|
+
/**
|
|
14
|
+
* Import specifications from CSV
|
|
15
|
+
*/
|
|
16
|
+
export declare function importSpecsFromCSV(csvContent: string, defaultLocale?: string): Promise<{
|
|
17
|
+
specs: ProductSpecs | null;
|
|
18
|
+
validation: ImportValidationResult;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Import specifications from JSON
|
|
22
|
+
*/
|
|
23
|
+
export declare function importSpecsFromJSON(jsonContent: string): Promise<{
|
|
24
|
+
specs: ProductSpecs | null;
|
|
25
|
+
validation: ImportValidationResult;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Merge imported specs with existing specs
|
|
29
|
+
*/
|
|
30
|
+
export declare function mergeSpecs(existing: ProductSpecs, imported: ProductSpecs, strategy?: 'replace' | 'merge' | 'append'): ProductSpecs;
|