@haus-tech/product-import-export-plugin 3.6.2 → 3.6.3
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/CHANGELOG.md +38 -11
- package/README.mdx +69 -59
- package/package.json +7 -9
- package/src/api/plugin.controller.d.ts +32 -2
- package/src/api/plugin.controller.js +30 -4
- package/src/api/plugin.controller.js.map +1 -1
- package/src/api/product-export.controller.d.ts +1 -0
- package/src/api/product-export.controller.js +17 -0
- package/src/api/product-export.controller.js.map +1 -1
- package/src/api/product-import.controller.js +1 -0
- package/src/api/product-import.controller.js.map +1 -1
- package/src/dashboard/bulk-export.tsx +0 -1
- package/src/dashboard/export-dialog.tsx +285 -22
- package/src/dashboard/export.tsx +17 -52
- package/src/dashboard/exported-list.tsx +96 -176
- package/src/dashboard/import.tsx +149 -286
- package/src/dashboard/index.tsx +28 -27
- package/src/dashboard/utils.ts +20 -3
- package/src/index.d.ts +1 -1
- package/src/product-import-export.plugin.d.ts +0 -6
- package/src/product-import-export.plugin.js +25 -29
- package/src/product-import-export.plugin.js.map +1 -1
- package/src/providers/import-providers/asset-importer.js +8 -27
- package/src/providers/import-providers/asset-importer.js.map +1 -1
- package/src/providers/import-providers/import-parser.d.ts +0 -1
- package/src/providers/import-providers/import-parser.js +2 -18
- package/src/providers/import-providers/import-parser.js.map +1 -1
- package/src/providers/import-providers/product-importer.d.ts +0 -10
- package/src/providers/import-providers/product-importer.js +34 -115
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/export-storage/export-storage-strategy.d.ts +0 -8
- package/src/services/extended-fast-importer.service.js +12 -20
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/import-storage/import-job-storage-strategy.d.ts +0 -9
- package/src/services/product-export-queue.service.js +1 -1
- package/src/services/product-export-queue.service.js.map +1 -1
- package/src/services/product-export.service.d.ts +13 -7
- package/src/services/product-export.service.js +96 -43
- package/src/services/product-export.service.js.map +1 -1
- package/src/types.d.ts +29 -9
- package/src/ui/components/product-import.component.ts +1 -0
- package/README.md +0 -231
- package/src/dashboard/export-configuration-panel.tsx +0 -404
- package/src/dashboard/use-active-channel-key.ts +0 -7
package/README.md
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
<!-- This file is generated from README.mdx by scripts/update-readmes.ts. Do not edit by hand. -->
|
|
2
|
-
|
|
3
|
-
# Product Import Export Plugin
|
|
4
|
-
|
|
5
|
-
A Vendure plugin for importing and exporting product data via CSV. Supports bulk product management and data migration between environments.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Import** – Bulk upload products from CSV with validation, custom fields, assets, facets, and variants
|
|
10
|
-
- **Export** – Export products to CSV with selected fields; export all products or a selection
|
|
11
|
-
- **Storage strategies** – Store exported files in S3 or locally on disk (with automatic fallback)
|
|
12
|
-
- **UI** – Admin UI and Dashboard integration for import and export
|
|
13
|
-
|
|
14
|
-

|
|
15
|
-
|
|
16
|
-
## Compatibility
|
|
17
|
-
|
|
18
|
-
Vendure **^3.6.0**
|
|
19
|
-
|
|
20
|
-
## Getting started
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @haus-tech/product-import-export-plugin
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Configuration
|
|
27
|
-
|
|
28
|
-
Add the plugin to your Vendure configuration.
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
export const config = {
|
|
32
|
-
plugins: [
|
|
33
|
-
ProductImportExportPlugin.init({
|
|
34
|
-
importOptions: {}, // Configure import options
|
|
35
|
-
exportOptions: {
|
|
36
|
-
storageStrategy: 's3', // 's3' | 'disk'
|
|
37
|
-
s3Options: {
|
|
38
|
-
bucket: 'your-bucket',
|
|
39
|
-
region: 'your-region',
|
|
40
|
-
credentials: {
|
|
41
|
-
accessKeyId: '...',
|
|
42
|
-
secretAccessKey: '...',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
],
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Configuration Options
|
|
52
|
-
|
|
53
|
-
### Import options
|
|
54
|
-
|
|
55
|
-
| Option | Type | Default | Description |
|
|
56
|
-
| ------------------------ | -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
-
| `updateProductSlug` | `boolean` | `true` | Update product slug from the `name` field in the CSV |
|
|
58
|
-
| `restoreSoftDeleted` | `boolean` | `true` | Restore soft-deleted products/variants found by id or SKU during import. If `false`, new products will be created instead. |
|
|
59
|
-
| `storageStrategy` | `ImportJobStorageStrategy` | local disk | Strategy instance for queued import file storage and retrieval. |
|
|
60
|
-
| `storageStrategyFactory` | `(injector: Injector) => ImportJobStorageStrategy` | `undefined` | Factory for DI-aware strategy creation (recommended for S3/remote storage). |
|
|
61
|
-
| `importJobStorage` | `'local' \| 's3'` | `'local'` | Deprecated compatibility flag. Prefer `storageStrategy` / `storageStrategyFactory`. |
|
|
62
|
-
|
|
63
|
-
Available import storage strategies:
|
|
64
|
-
|
|
65
|
-
- `LocalImportJobStorageStrategy` (default)
|
|
66
|
-
- `S3ImportJobStorageStrategy`
|
|
67
|
-
|
|
68
|
-
### Export options
|
|
69
|
-
|
|
70
|
-
| Option | Type | Default | Description |
|
|
71
|
-
| ----------------------- | --------------------- | ----------------------- | ------------------------------------------------------------ |
|
|
72
|
-
| `defaultFileName` | `string` | `'products_export.csv'` | Default filename for exports |
|
|
73
|
-
| `exportAssetsAsOptions` | `('url' \| 'json')[]` | `['url', 'json']` | Available formats for asset export |
|
|
74
|
-
| `defaultExportAssetsAs` | `'url' \| 'json'` | `'url'` | Default asset export format |
|
|
75
|
-
| `defaultExportFields` | `string[]` | See above | Fields that will be pre-selected in the export UI |
|
|
76
|
-
| `requiredExportFields` | `string[]` | `['name', 'sku']` | Fields that must always be included |
|
|
77
|
-
| `storageStrategy` | `'s3' \| 'disk'` | `'disk'` | Where exported files are stored |
|
|
78
|
-
| `s3Options` | `object` | `undefined` | S3 configuration (required when `storageStrategy` is `'s3'`) |
|
|
79
|
-
|
|
80
|
-
### Storage strategy
|
|
81
|
-
|
|
82
|
-
The plugin supports configurable storage for exported files:
|
|
83
|
-
|
|
84
|
-
- **S3** – Stores export files in an S3-compatible bucket
|
|
85
|
-
- **Disk (default)** – Saves files locally on the server
|
|
86
|
-
|
|
87
|
-
## CSV Format for Import
|
|
88
|
-
|
|
89
|
-
### Required columns
|
|
90
|
-
|
|
91
|
-
- `name` – Product name (use `name:en`, `name:sv` etc. for multiple languages)
|
|
92
|
-
- `sku` – Variant SKU (unique per variant)
|
|
93
|
-
|
|
94
|
-
### Optional columns
|
|
95
|
-
|
|
96
|
-
| Column | Format | Example |
|
|
97
|
-
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
98
|
-
| `name`, `slug`, `description` | Text, or `name:en`, `name:sv` for translations | `My Product` |
|
|
99
|
-
| `assets` | Pipe-separated URLs, or JSON `[{"url":"...","name":"..."}]` | `https://example.com/1.jpg\|https://example.com/2.jpg` |
|
|
100
|
-
| `facets` | `facet:value` pairs, pipe-separated | `Brand:Acme\|Category:Electronics` |
|
|
101
|
-
| `optionGroups` | Pipe-separated group names. Use `name:code` to share a group across products (see [Sharing option groups](#sharing-option-groups)) | `Size\|Color` or `size:shoe-size` |
|
|
102
|
-
| `optionValues` | Pipe-separated values (order matches option groups) | `Large\|Blue` |
|
|
103
|
-
| `price` | Number (in smallest currency unit) | `9999` |
|
|
104
|
-
| `taxCategory` | Tax category name | `standard` |
|
|
105
|
-
| `stockOnHand` | Number | `100` |
|
|
106
|
-
| `trackInventory` | `true` or `false` | `true` |
|
|
107
|
-
| `variantAssets` | Same format as `assets` | `https://example.com/variant.jpg` |
|
|
108
|
-
| `variantFacets` | Same format as `facets` | `Material:Cotton` |
|
|
109
|
-
| `enabled` | `true` or `false` | `true` |
|
|
110
|
-
| Custom fields | Column name = custom field name; value depends on field type | Add columns for any Product or ProductVariant custom fields defined in your Vendure config |
|
|
111
|
-
|
|
112
|
-
#### Import behavior rules
|
|
113
|
-
|
|
114
|
-
- If a column is omitted from the CSV header, that field is left unchanged on existing products/variants.
|
|
115
|
-
- If a column exists in the header but the cell is empty, that field is explicitly cleared.
|
|
116
|
-
- For products with more than one variant, `optionGroups` and `optionValues` are required.
|
|
117
|
-
|
|
118
|
-
#### Sharing option groups
|
|
119
|
-
|
|
120
|
-
By default, each product gets its own option groups. To **share an option group** across
|
|
121
|
-
multiple products (e.g. a common "Size" group for all shoes), use the `name:code` syntax in the
|
|
122
|
-
`optionGroups` column: `size:shoe-size`. All products that reference the same code will share
|
|
123
|
-
a single option group. Products using a plain name (e.g. just `size`) will each get their own
|
|
124
|
-
independent option group.
|
|
125
|
-
|
|
126
|
-
```csv
|
|
127
|
-
...optionGroups , optionValues , ...
|
|
128
|
-
# Each shoe product shares a single "shoe-size" option group:
|
|
129
|
-
"size:shoe-size" , Small , ...
|
|
130
|
-
"size:shoe-size" , Medium , ...
|
|
131
|
-
# This product gets its own independent "size" group:
|
|
132
|
-
"size" , 4GB , ...
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
The first product to reference a given code defines the full set of option values for that
|
|
136
|
-
shared group; subsequent products may use any subset of those values. Codes are language-
|
|
137
|
-
agnostic — only the `name` portion before the colon is translated per language column
|
|
138
|
-
(`optionGroups:en`, `optionGroups:sv`, etc.).
|
|
139
|
-
|
|
140
|
-
#### Multi-language support
|
|
141
|
-
|
|
142
|
-
Use language-specific columns by suffixing the field name with the language code. The language codes must match those configured in your Vendure instance (e.g. `en`, `sv`).
|
|
143
|
-
|
|
144
|
-
| Column pattern | Example columns | Use case |
|
|
145
|
-
| ------------------------ | ---------------------- | ------------------------------------------------ |
|
|
146
|
-
| `name:en`, `name:sv` | Product name per lang | Translated product names |
|
|
147
|
-
| `slug:en`, `slug:sv` | Slug per lang | Translated URLs |
|
|
148
|
-
| `description:en` | Description per lang | Translated descriptions |
|
|
149
|
-
| `facets:en`, `facets:sv` | Facets per lang | Translated facet names and values |
|
|
150
|
-
| `optionGroups:en` | Option groups per lang | Translated option group names (e.g. Size, Color) |
|
|
151
|
-
| `optionValues:en` | Option values per lang | Translated option values (e.g. Large, Blue) |
|
|
152
|
-
|
|
153
|
-
Example CSV with English and Swedish:
|
|
154
|
-
|
|
155
|
-
```csv
|
|
156
|
-
name:en,name:sv,sku,facets:en,facets:sv
|
|
157
|
-
"Red Shirt","Röd tröja",SHIRT-001,"Color:Red|Size:Large","Färg:Röd|Storlek:Stor"
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### Multiple values (pipe separator)
|
|
161
|
-
|
|
162
|
-
Fields that can have multiple values use the pipe character `|` as a separator:
|
|
163
|
-
|
|
164
|
-
- **Facets**: `Brand:Acme|Category:Electronics`
|
|
165
|
-
- **Option groups**: `Size|Color`
|
|
166
|
-
- **Option values**: `Large|Blue` (order matches option groups)
|
|
167
|
-
- **Assets**: `https://example.com/1.jpg|https://example.com/2.jpg`
|
|
168
|
-
|
|
169
|
-
## Usage
|
|
170
|
-
|
|
171
|
-
:::note
|
|
172
|
-
The Admin UI offers basic import and export. Some features such as the export-all option and the list of exported files are only available in the Dashboard. New features will be added to the Dashboard only.
|
|
173
|
-
:::
|
|
174
|
-
|
|
175
|
-
### Importing products
|
|
176
|
-
|
|
177
|
-
1. Create a CSV with the columns above.
|
|
178
|
-
2. Go to the Admin UI or Dashboard and open the Product Import section.
|
|
179
|
-
3. Upload the CSV and choose options (update slugs, restore soft-deleted, main language).
|
|
180
|
-
4. Select the update strategy: **merge** (keep existing facets/assets) or **replace** (overwrite).
|
|
181
|
-
5. Start the import.
|
|
182
|
-
|
|
183
|
-
### Exporting products
|
|
184
|
-
|
|
185
|
-
1. In the Admin UI or Dashboard, select products to export (or use bulk export).
|
|
186
|
-
2. Configure export fields (assets, facets, custom fields, etc.).
|
|
187
|
-
- For products with more than one variant, `optionGroups` and `optionValues` must be included.
|
|
188
|
-
- `productId` is not available as a selectable export field.
|
|
189
|
-
- If `customFields` are not selected, no custom-field columns are written to the CSV.
|
|
190
|
-
3. Choose asset format: URL or JSON.
|
|
191
|
-
4. Start the export (it is added to a job queue and runs in the background).
|
|
192
|
-
5. Download the CSV when the export is complete. Your downloaded files will be listed in the export view.
|
|
193
|
-
|
|
194
|
-

|
|
195
|
-
|
|
196
|
-
## Optional: Email notification on export complete
|
|
197
|
-
|
|
198
|
-
If you use `@vendure/email-plugin`, you can add an email notification when an export finishes from the `/email` subpath.
|
|
199
|
-
|
|
200
|
-
The plugin now provides a default fallback template for `product-export-complete/body.hbs`.
|
|
201
|
-
If you define your own template in your app's template directory, your template takes precedence.
|
|
202
|
-
Note: templates for `@vendure/email-plugin` should be valid MJML.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import path from 'path'
|
|
206
|
-
import {
|
|
207
|
-
DefaultEmailPlugin,
|
|
208
|
-
FileBasedTemplateLoader,
|
|
209
|
-
defaultEmailHandlers,
|
|
210
|
-
} from '@vendure/email-plugin'
|
|
211
|
-
import {
|
|
212
|
-
withProductExportedHandler,
|
|
213
|
-
withProductExportedTemplateFallback,
|
|
214
|
-
} from '@haus-tech/product-import-export-plugin/email'
|
|
215
|
-
|
|
216
|
-
export const config = {
|
|
217
|
-
plugins: [
|
|
218
|
-
DefaultEmailPlugin.init({
|
|
219
|
-
handlers: withProductExportedHandler(defaultEmailHandlers),
|
|
220
|
-
templateLoader: withProductExportedTemplateFallback(
|
|
221
|
-
new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
|
|
222
|
-
),
|
|
223
|
-
}),
|
|
224
|
-
],
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Resources
|
|
229
|
-
|
|
230
|
-
- [Vendure Plugin Documentation](https://docs.vendure.io/guides/developer-guide/plugins/)
|
|
231
|
-
- [Vendure Import/Export Guide](https://docs.vendure.io/guides/developer-guide/importing-data/)
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Accordion,
|
|
3
|
-
AccordionContent,
|
|
4
|
-
AccordionItem,
|
|
5
|
-
AccordionTrigger,
|
|
6
|
-
Checkbox,
|
|
7
|
-
Input,
|
|
8
|
-
Label,
|
|
9
|
-
Popover,
|
|
10
|
-
PopoverContent,
|
|
11
|
-
PopoverTrigger,
|
|
12
|
-
RadioGroup,
|
|
13
|
-
RadioGroupItem,
|
|
14
|
-
toast,
|
|
15
|
-
} from '@vendure/dashboard'
|
|
16
|
-
import { ChevronRight, InfoIcon } from 'lucide-react'
|
|
17
|
-
import { useCallback, useEffect, useImperativeHandle, useMemo, forwardRef, useState } from 'react'
|
|
18
|
-
import { useActiveChannelKey } from './use-active-channel-key'
|
|
19
|
-
import {
|
|
20
|
-
getServerLocation,
|
|
21
|
-
getChannelHeader,
|
|
22
|
-
type PluginInitOptions,
|
|
23
|
-
type ExportFields,
|
|
24
|
-
type ProductFields,
|
|
25
|
-
type VariantFields,
|
|
26
|
-
} from './utils'
|
|
27
|
-
|
|
28
|
-
const MANDATORY_OPTION_FIELDS: Array<ProductFields | VariantFields> = [
|
|
29
|
-
'optionGroups',
|
|
30
|
-
'optionValues',
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
const AVAILABLE_EXPORT_FIELDS: ExportFields = [
|
|
34
|
-
'name',
|
|
35
|
-
'slug',
|
|
36
|
-
'description',
|
|
37
|
-
'assets',
|
|
38
|
-
'facets',
|
|
39
|
-
'optionGroups',
|
|
40
|
-
'sku',
|
|
41
|
-
'optionValues',
|
|
42
|
-
'price',
|
|
43
|
-
'taxCategory',
|
|
44
|
-
'stockOnHand',
|
|
45
|
-
'trackInventory',
|
|
46
|
-
'variantAssets',
|
|
47
|
-
'variantFacets',
|
|
48
|
-
'enabled',
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
export type ExportConfigurationPanelProps = {
|
|
52
|
-
/** Selected product IDs for scoped export; omit when `isExportAll` (treated as empty). */
|
|
53
|
-
productIds?: string[]
|
|
54
|
-
isExportAll: boolean
|
|
55
|
-
/** Fetch config + fields only while true (e.g. dialog `open`) */
|
|
56
|
-
active: boolean
|
|
57
|
-
/** Prefix for input/checkbox ids so modal vs page never collide */
|
|
58
|
-
idPrefix?: string
|
|
59
|
-
/** Expand “Select fields to export” on first paint (e.g. modal); inline page keeps it collapsed */
|
|
60
|
-
fieldsAccordionDefaultOpen?: boolean
|
|
61
|
-
onExportSuccess?: () => void
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type ExportConfigurationPanelHandle = {
|
|
65
|
-
submitExport: () => Promise<void>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const ExportConfigurationPanel = forwardRef<
|
|
69
|
-
ExportConfigurationPanelHandle,
|
|
70
|
-
ExportConfigurationPanelProps
|
|
71
|
-
>(function ExportConfigurationPanel(
|
|
72
|
-
{
|
|
73
|
-
productIds,
|
|
74
|
-
isExportAll,
|
|
75
|
-
active,
|
|
76
|
-
idPrefix = 'export-config',
|
|
77
|
-
fieldsAccordionDefaultOpen = false,
|
|
78
|
-
onExportSuccess,
|
|
79
|
-
},
|
|
80
|
-
ref,
|
|
81
|
-
) {
|
|
82
|
-
const resolvedProductIds = productIds ?? []
|
|
83
|
-
const activeChannelId = useActiveChannelKey()
|
|
84
|
-
|
|
85
|
-
const [config, setConfig] = useState<PluginInitOptions['exportOptions'] | null>(null)
|
|
86
|
-
|
|
87
|
-
const [fileName, setFileName] = useState('')
|
|
88
|
-
const [customFieldNames, setCustomFieldNames] = useState<string[]>([])
|
|
89
|
-
const [selectedCustomFields, setSelectedCustomFields] = useState<string[]>([])
|
|
90
|
-
const [exportAssetsAs, setExportAssetsAs] = useState<'url' | 'json'>('url')
|
|
91
|
-
const [selectedExportFields, setSelectedExportFields] = useState<ExportFields>([])
|
|
92
|
-
const [toggleAllChecked, setToggleAllChecked] = useState(false)
|
|
93
|
-
|
|
94
|
-
/** Stable primitive so effects / callbacks don’t churn on fresh array references with the same IDs. */
|
|
95
|
-
const productIdsFetchKey = resolvedProductIds.join(',')
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (!active || !activeChannelId) return
|
|
99
|
-
|
|
100
|
-
const fetchInitial = async () => {
|
|
101
|
-
try {
|
|
102
|
-
const serverPath = getServerLocation()
|
|
103
|
-
const [configRes, customFieldsRes] = await Promise.all([
|
|
104
|
-
fetch(`${serverPath}/product-import-export/config`, {
|
|
105
|
-
method: 'GET',
|
|
106
|
-
headers: {
|
|
107
|
-
'Content-Type': 'application/json',
|
|
108
|
-
...getChannelHeader(),
|
|
109
|
-
},
|
|
110
|
-
credentials: 'include',
|
|
111
|
-
}),
|
|
112
|
-
fetch(`${serverPath}/product-export/custom-fields`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
headers: {
|
|
115
|
-
'Content-Type': 'application/json',
|
|
116
|
-
...getChannelHeader(),
|
|
117
|
-
},
|
|
118
|
-
credentials: 'include',
|
|
119
|
-
body: JSON.stringify(resolvedProductIds.length > 0 ? resolvedProductIds : []),
|
|
120
|
-
}),
|
|
121
|
-
])
|
|
122
|
-
|
|
123
|
-
const configData = await configRes.json()
|
|
124
|
-
const customFieldsData = await customFieldsRes.json()
|
|
125
|
-
|
|
126
|
-
setConfig(configData.exportOptions)
|
|
127
|
-
|
|
128
|
-
const names = customFieldsData.map((field: { name: string }) => field.name)
|
|
129
|
-
setCustomFieldNames(names)
|
|
130
|
-
setSelectedCustomFields([...names])
|
|
131
|
-
|
|
132
|
-
const defaultAssets =
|
|
133
|
-
configData.exportOptions?.defaultExportAssetsAs ||
|
|
134
|
-
configData.exportOptions?.exportAssetsAsOptions?.[0] ||
|
|
135
|
-
'url'
|
|
136
|
-
setExportAssetsAs(defaultAssets)
|
|
137
|
-
|
|
138
|
-
const initialExportFields = Array.from(
|
|
139
|
-
new Set([
|
|
140
|
-
...(configData.exportOptions?.defaultExportFields || []),
|
|
141
|
-
...(configData.exportOptions?.requiredExportFields || []),
|
|
142
|
-
...MANDATORY_OPTION_FIELDS,
|
|
143
|
-
]),
|
|
144
|
-
) as ExportFields
|
|
145
|
-
|
|
146
|
-
setSelectedExportFields(initialExportFields)
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.error('Failed to fetch export config:', error)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
fetchInitial()
|
|
153
|
-
}, [active, activeChannelId, productIdsFetchKey])
|
|
154
|
-
|
|
155
|
-
const derivedToggleAll = useMemo(() => {
|
|
156
|
-
const allStandard = AVAILABLE_EXPORT_FIELDS.every((f) => selectedExportFields.includes(f))
|
|
157
|
-
const allCustom =
|
|
158
|
-
customFieldNames.length === 0 ||
|
|
159
|
-
customFieldNames.every((f) => selectedCustomFields.includes(f))
|
|
160
|
-
return allStandard && allCustom
|
|
161
|
-
}, [selectedExportFields, selectedCustomFields, customFieldNames])
|
|
162
|
-
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
setToggleAllChecked(derivedToggleAll)
|
|
165
|
-
}, [derivedToggleAll])
|
|
166
|
-
|
|
167
|
-
const toggleCustomField = (fieldName: string) => {
|
|
168
|
-
setSelectedCustomFields((prev) =>
|
|
169
|
-
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const toggleExportField = (fieldName: ProductFields | VariantFields) => {
|
|
174
|
-
if (MANDATORY_OPTION_FIELDS.includes(fieldName)) return
|
|
175
|
-
setSelectedExportFields((prev) =>
|
|
176
|
-
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const toggleSelectAll = (checked: boolean) => {
|
|
181
|
-
setToggleAllChecked(checked)
|
|
182
|
-
if (checked) {
|
|
183
|
-
setSelectedExportFields([...AVAILABLE_EXPORT_FIELDS])
|
|
184
|
-
setSelectedCustomFields([...customFieldNames])
|
|
185
|
-
} else {
|
|
186
|
-
setSelectedExportFields([...(config?.requiredExportFields || []), ...MANDATORY_OPTION_FIELDS])
|
|
187
|
-
setSelectedCustomFields([])
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const bothAssetModes =
|
|
192
|
-
config?.exportAssetsAsOptions?.includes('url') &&
|
|
193
|
-
config?.exportAssetsAsOptions?.includes('json')
|
|
194
|
-
|
|
195
|
-
const submitExport = useCallback(async () => {
|
|
196
|
-
try {
|
|
197
|
-
const serverPath = getServerLocation()
|
|
198
|
-
const trimmed = fileName.trim()
|
|
199
|
-
const finalFileName = trimmed || config?.defaultFileName || 'products_export.csv'
|
|
200
|
-
const endpoint = isExportAll ? 'export-all' : 'export'
|
|
201
|
-
const body = isExportAll ? undefined : JSON.stringify(resolvedProductIds)
|
|
202
|
-
|
|
203
|
-
const res = await fetch(
|
|
204
|
-
`${serverPath}/product-export/${endpoint}?fileName=${encodeURIComponent(
|
|
205
|
-
finalFileName,
|
|
206
|
-
)}&customFields=${encodeURIComponent(
|
|
207
|
-
selectedCustomFields.join(','),
|
|
208
|
-
)}&exportAssetsAs=${exportAssetsAs}&selectedExportFields=${encodeURIComponent(
|
|
209
|
-
selectedExportFields.join(','),
|
|
210
|
-
)}`,
|
|
211
|
-
{
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers: {
|
|
214
|
-
'Content-Type': 'application/json',
|
|
215
|
-
...getChannelHeader(),
|
|
216
|
-
},
|
|
217
|
-
credentials: 'include',
|
|
218
|
-
...(body && { body }),
|
|
219
|
-
},
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
if (!res.ok) {
|
|
223
|
-
const json = await res.json().catch(() => ({}))
|
|
224
|
-
throw new Error(json?.message || 'Export failed')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
toast.success(
|
|
228
|
-
isExportAll
|
|
229
|
-
? 'Export queued. You will receive an email when the file is ready.'
|
|
230
|
-
: `Export job queued successfully. ${resolvedProductIds.length} products will be exported.`,
|
|
231
|
-
)
|
|
232
|
-
onExportSuccess?.()
|
|
233
|
-
} catch (error: unknown) {
|
|
234
|
-
console.error(error)
|
|
235
|
-
toast.error((error as Error)?.message || 'Failed to queue export')
|
|
236
|
-
}
|
|
237
|
-
}, [
|
|
238
|
-
fileName,
|
|
239
|
-
config?.defaultFileName,
|
|
240
|
-
isExportAll,
|
|
241
|
-
productIdsFetchKey,
|
|
242
|
-
selectedCustomFields,
|
|
243
|
-
exportAssetsAs,
|
|
244
|
-
selectedExportFields,
|
|
245
|
-
onExportSuccess,
|
|
246
|
-
])
|
|
247
|
-
|
|
248
|
-
useImperativeHandle(ref, () => ({ submitExport }), [submitExport])
|
|
249
|
-
|
|
250
|
-
if (!active) {
|
|
251
|
-
return null
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<div className="space-y-4">
|
|
256
|
-
<div>
|
|
257
|
-
<Label htmlFor={`${idPrefix}-file-name`}>File name (optional)</Label>
|
|
258
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
259
|
-
A timestamp is added automatically for uniqueness.
|
|
260
|
-
</p>
|
|
261
|
-
<Input
|
|
262
|
-
id={`${idPrefix}-file-name`}
|
|
263
|
-
value={fileName}
|
|
264
|
-
onChange={(e) => setFileName(e.target.value)}
|
|
265
|
-
placeholder={config?.defaultFileName || 'products_export.csv'}
|
|
266
|
-
className="mt-2"
|
|
267
|
-
/>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
<Accordion
|
|
271
|
-
className="border rounded-lg px-3"
|
|
272
|
-
defaultValue={fieldsAccordionDefaultOpen ? ['select-fields'] : ([] as string[])}
|
|
273
|
-
>
|
|
274
|
-
<AccordionItem value="select-fields" className="border-0">
|
|
275
|
-
<AccordionTrigger className="group/trigger flex w-full items-center gap-3 py-3 text-sm hover:no-underline [&>svg:last-child]:size-4 [&>svg:last-child]:shrink-0 [&>svg:last-child]:self-center">
|
|
276
|
-
<ChevronRight className="text-primary size-4 shrink-0 self-center transition-transform duration-200 group-data-[state=open]/trigger:rotate-90" />
|
|
277
|
-
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-1 self-center text-left leading-snug">
|
|
278
|
-
<span className="font-medium text-foreground">Configure fields to export</span>
|
|
279
|
-
</span>
|
|
280
|
-
</AccordionTrigger>
|
|
281
|
-
<AccordionContent className="space-y-4 pb-4 pt-4 border-t border-border">
|
|
282
|
-
<div className="flex items-center space-x-2 pt-2">
|
|
283
|
-
<Checkbox
|
|
284
|
-
id={`${idPrefix}-select-all`}
|
|
285
|
-
checked={toggleAllChecked}
|
|
286
|
-
onCheckedChange={(checked) => toggleSelectAll(checked === true)}
|
|
287
|
-
/>
|
|
288
|
-
<Label htmlFor={`${idPrefix}-select-all`} className="font-normal">
|
|
289
|
-
Select all fields
|
|
290
|
-
</Label>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
294
|
-
{AVAILABLE_EXPORT_FIELDS.map((field) => (
|
|
295
|
-
<div key={field} className="flex items-center space-x-2">
|
|
296
|
-
<Checkbox
|
|
297
|
-
id={`${idPrefix}-field-${field}`}
|
|
298
|
-
checked={selectedExportFields.includes(field)}
|
|
299
|
-
onCheckedChange={() => toggleExportField(field)}
|
|
300
|
-
disabled={
|
|
301
|
-
config?.requiredExportFields?.includes(field) ||
|
|
302
|
-
MANDATORY_OPTION_FIELDS.includes(field)
|
|
303
|
-
}
|
|
304
|
-
/>
|
|
305
|
-
<Label htmlFor={`${idPrefix}-field-${field}`} className="font-normal">
|
|
306
|
-
{field}
|
|
307
|
-
</Label>
|
|
308
|
-
</div>
|
|
309
|
-
))}
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
{customFieldNames.length > 0 && (
|
|
313
|
-
<div className="space-y-2 pt-4 border-t">
|
|
314
|
-
<Label className="text-muted-foreground pb-2">Custom fields</Label>
|
|
315
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
316
|
-
{customFieldNames.map((field) => (
|
|
317
|
-
<div key={field} className="flex items-center space-x-2">
|
|
318
|
-
<Checkbox
|
|
319
|
-
id={`${idPrefix}-custom-${field}`}
|
|
320
|
-
checked={selectedCustomFields.includes(field)}
|
|
321
|
-
onCheckedChange={() => toggleCustomField(field)}
|
|
322
|
-
/>
|
|
323
|
-
<Label htmlFor={`${idPrefix}-custom-${field}`} className="font-normal">
|
|
324
|
-
{field}
|
|
325
|
-
</Label>
|
|
326
|
-
</div>
|
|
327
|
-
))}
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
|
-
</AccordionContent>
|
|
332
|
-
</AccordionItem>
|
|
333
|
-
</Accordion>
|
|
334
|
-
|
|
335
|
-
{bothAssetModes ? (
|
|
336
|
-
<Accordion className="border rounded-lg px-3" defaultValue={[]}>
|
|
337
|
-
<AccordionItem value="advanced-settings" className="border-0">
|
|
338
|
-
<AccordionTrigger className="group/trigger flex w-full items-center gap-3 py-3 text-sm hover:no-underline [&>svg:last-child]:size-4 [&>svg:last-child]:shrink-0 [&>svg:last-child]:self-center">
|
|
339
|
-
<ChevronRight className="text-primary size-4 shrink-0 self-center transition-transform duration-200 group-data-[state=open]/trigger:rotate-90" />
|
|
340
|
-
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-1 self-center text-left leading-snug">
|
|
341
|
-
<span className="font-medium text-foreground">Advanced settings</span>
|
|
342
|
-
</span>
|
|
343
|
-
</AccordionTrigger>
|
|
344
|
-
<AccordionContent className="space-y-4 pb-4 pt-4 border-t border-border">
|
|
345
|
-
<div className="flex flex-row flex-wrap items-center gap-x-6 gap-y-2 pt-2">
|
|
346
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
347
|
-
<Label className="font-medium">Export assets as</Label>
|
|
348
|
-
<Popover>
|
|
349
|
-
<PopoverTrigger
|
|
350
|
-
type="button"
|
|
351
|
-
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
352
|
-
aria-label="What does export assets as mean?"
|
|
353
|
-
>
|
|
354
|
-
<InfoIcon className="h-4 w-4" />
|
|
355
|
-
</PopoverTrigger>
|
|
356
|
-
<PopoverContent className="w-80 text-sm" align="start" side="top">
|
|
357
|
-
<p className="font-medium text-foreground">Asset columns in the CSV</p>
|
|
358
|
-
<p className="mt-2 text-muted-foreground">
|
|
359
|
-
Controls how product and variant asset fields are serialized in the export.
|
|
360
|
-
</p>
|
|
361
|
-
<ul className="mt-3 list-disc space-y-2 pl-4 text-muted-foreground">
|
|
362
|
-
<li>
|
|
363
|
-
<span className="font-medium text-foreground">URL only</span> — each cell
|
|
364
|
-
lists asset file URLs separated by{' '}
|
|
365
|
-
<span className="font-mono text-xs">|</span>.
|
|
366
|
-
</li>
|
|
367
|
-
<li>
|
|
368
|
-
<span className="font-medium text-foreground">Full JSON</span> — each cell
|
|
369
|
-
contains JSON with <span className="font-mono text-xs">id</span>,{' '}
|
|
370
|
-
<span className="font-mono text-xs">name</span>, and{' '}
|
|
371
|
-
<span className="font-mono text-xs">url</span> per asset.
|
|
372
|
-
</li>
|
|
373
|
-
</ul>
|
|
374
|
-
</PopoverContent>
|
|
375
|
-
</Popover>
|
|
376
|
-
</div>
|
|
377
|
-
<RadioGroup
|
|
378
|
-
value={exportAssetsAs}
|
|
379
|
-
onValueChange={(val) => setExportAssetsAs(val as 'url' | 'json')}
|
|
380
|
-
className="flex flex-row flex-wrap gap-6"
|
|
381
|
-
>
|
|
382
|
-
<div className="flex items-center space-x-2">
|
|
383
|
-
<RadioGroupItem value="url" id={`${idPrefix}-assets-url`} />
|
|
384
|
-
<Label htmlFor={`${idPrefix}-assets-url`} className="font-normal">
|
|
385
|
-
URL only
|
|
386
|
-
</Label>
|
|
387
|
-
</div>
|
|
388
|
-
<div className="flex items-center space-x-2">
|
|
389
|
-
<RadioGroupItem value="json" id={`${idPrefix}-assets-json`} />
|
|
390
|
-
<Label htmlFor={`${idPrefix}-assets-json`} className="font-normal">
|
|
391
|
-
Full JSON
|
|
392
|
-
</Label>
|
|
393
|
-
</div>
|
|
394
|
-
</RadioGroup>
|
|
395
|
-
</div>
|
|
396
|
-
</AccordionContent>
|
|
397
|
-
</AccordionItem>
|
|
398
|
-
</Accordion>
|
|
399
|
-
) : null}
|
|
400
|
-
</div>
|
|
401
|
-
)
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
ExportConfigurationPanel.displayName = 'ExportConfigurationPanel'
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { useChannel } from '@vendure/dashboard'
|
|
2
|
-
|
|
3
|
-
/** Changes when the admin switches channel — use as a useEffect dependency to refetch channel-scoped data. */
|
|
4
|
-
export function useActiveChannelKey(): string | undefined {
|
|
5
|
-
const { activeChannel } = useChannel()
|
|
6
|
-
return activeChannel?.id
|
|
7
|
-
}
|