@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
|
@@ -6,18 +6,27 @@ import {
|
|
|
6
6
|
DialogFooter,
|
|
7
7
|
DialogHeader,
|
|
8
8
|
DialogTitle,
|
|
9
|
+
Input,
|
|
10
|
+
Label,
|
|
11
|
+
RadioGroup,
|
|
12
|
+
RadioGroupItem,
|
|
13
|
+
Checkbox,
|
|
9
14
|
} from '@vendure/dashboard'
|
|
10
|
-
import {
|
|
15
|
+
import { useState, useEffect } from 'react'
|
|
16
|
+
import { toast } from 'sonner'
|
|
11
17
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
getServerLocation,
|
|
19
|
+
getChannelHeader,
|
|
20
|
+
type PluginInitOptions,
|
|
21
|
+
type ExportFields,
|
|
22
|
+
type ProductFields,
|
|
23
|
+
type VariantFields,
|
|
24
|
+
} from './utils'
|
|
15
25
|
|
|
16
26
|
interface ExportDialogProps {
|
|
17
27
|
open: boolean
|
|
18
28
|
onOpenChange: (open: boolean) => void
|
|
19
|
-
|
|
20
|
-
productIds?: string[]
|
|
29
|
+
productIds: string[]
|
|
21
30
|
isExportAll?: boolean
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -27,15 +36,173 @@ export function ExportDialog({
|
|
|
27
36
|
productIds,
|
|
28
37
|
isExportAll = false,
|
|
29
38
|
}: ExportDialogProps) {
|
|
30
|
-
const
|
|
31
|
-
const [
|
|
39
|
+
const mandatoryOptionFields: Array<ProductFields | VariantFields> = ['optionGroups', 'optionValues']
|
|
40
|
+
const [fileName, setFileName] = useState('')
|
|
41
|
+
const [customFields, setCustomFields] = useState<string[]>([])
|
|
42
|
+
const [selectedFields, setSelectedFields] = useState<string[]>([])
|
|
43
|
+
const [exportAssetsAs, setExportAssetsAs] = useState<'url' | 'json'>('url')
|
|
44
|
+
const [selectedExportFields, setSelectedExportFields] = useState<ExportFields>([])
|
|
45
|
+
const [customColumnNames, setCustomColumnNames] = useState<string[]>([])
|
|
46
|
+
const [selectedCustomColumns, setSelectedCustomColumns] = useState<string[]>([])
|
|
47
|
+
const [availableExportFields] = useState<ExportFields>([
|
|
48
|
+
'id',
|
|
49
|
+
'name',
|
|
50
|
+
'slug',
|
|
51
|
+
'description',
|
|
52
|
+
'assets',
|
|
53
|
+
'facets',
|
|
54
|
+
'optionGroups',
|
|
55
|
+
'sku',
|
|
56
|
+
'optionValues',
|
|
57
|
+
'price',
|
|
58
|
+
'taxCategory',
|
|
59
|
+
'stockOnHand',
|
|
60
|
+
'trackInventory',
|
|
61
|
+
'variantAssets',
|
|
62
|
+
'variantFacets',
|
|
63
|
+
'enabled',
|
|
64
|
+
])
|
|
65
|
+
const [config, setConfig] = useState<PluginInitOptions['exportOptions'] | null>(null)
|
|
66
|
+
const [toggleAllChecked, setToggleAllChecked] = useState(false)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const fetchConfig = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const serverPath = getServerLocation()
|
|
72
|
+
const [configRes, customFieldsRes] = await Promise.all([
|
|
73
|
+
fetch(`${serverPath}/product-import-export/config`, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
...getChannelHeader(),
|
|
78
|
+
},
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
}),
|
|
81
|
+
fetch(`${serverPath}/product-export/custom-fields`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
...getChannelHeader(),
|
|
86
|
+
},
|
|
87
|
+
credentials: 'include',
|
|
88
|
+
body: JSON.stringify(productIds.length > 0 ? productIds : []),
|
|
89
|
+
}),
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
const configData = await configRes.json()
|
|
93
|
+
const customFieldsData = await customFieldsRes.json()
|
|
94
|
+
|
|
95
|
+
setConfig(configData.exportOptions)
|
|
96
|
+
setCustomFields(customFieldsData.map((field: { name: string }) => field.name))
|
|
97
|
+
setSelectedFields([...customFieldsData.map((field: { name: string }) => field.name)])
|
|
98
|
+
|
|
99
|
+
const columnNames =
|
|
100
|
+
configData.exportOptions?.customExportColumns?.map((c: { name: string }) => c.name) ?? []
|
|
101
|
+
setCustomColumnNames(columnNames)
|
|
102
|
+
setSelectedCustomColumns(columnNames)
|
|
103
|
+
|
|
104
|
+
setExportAssetsAs(
|
|
105
|
+
configData.exportOptions?.defaultExportAssetsAs ||
|
|
106
|
+
configData.exportOptions?.exportAssetsAsOptions?.[0] ||
|
|
107
|
+
'url',
|
|
108
|
+
)
|
|
109
|
+
if (
|
|
110
|
+
configData.exportOptions?.defaultExportFields ||
|
|
111
|
+
configData.exportOptions?.requiredExportFields
|
|
112
|
+
) {
|
|
113
|
+
setSelectedExportFields(
|
|
114
|
+
Array.from(
|
|
115
|
+
new Set([
|
|
116
|
+
...(configData.exportOptions.defaultExportFields || []),
|
|
117
|
+
...(configData.exportOptions.requiredExportFields || []),
|
|
118
|
+
...mandatoryOptionFields,
|
|
119
|
+
]),
|
|
120
|
+
) as ExportFields,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Failed to fetch config:', error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (open) {
|
|
129
|
+
fetchConfig()
|
|
130
|
+
}
|
|
131
|
+
}, [open, productIds])
|
|
132
|
+
|
|
133
|
+
const toggleFieldSelection = (fieldName: string) => {
|
|
134
|
+
setSelectedFields((prev) =>
|
|
135
|
+
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const toggleCustomColumnSelection = (name: string) => {
|
|
140
|
+
setSelectedCustomColumns((prev) =>
|
|
141
|
+
prev.includes(name) ? prev.filter((c) => c !== name) : [...prev, name],
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const toggleExportFieldSelection = (fieldName: ProductFields | VariantFields) => {
|
|
146
|
+
if (mandatoryOptionFields.includes(fieldName)) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
setSelectedExportFields((prev) =>
|
|
150
|
+
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const toggleSelectAll = (checked: boolean) => {
|
|
155
|
+
setToggleAllChecked(checked)
|
|
156
|
+
if (checked) {
|
|
157
|
+
setSelectedExportFields([...availableExportFields])
|
|
158
|
+
setSelectedFields([...customFields])
|
|
159
|
+
setSelectedCustomColumns([...customColumnNames])
|
|
160
|
+
} else {
|
|
161
|
+
setSelectedExportFields([
|
|
162
|
+
...(config?.requiredExportFields || []),
|
|
163
|
+
...mandatoryOptionFields,
|
|
164
|
+
])
|
|
165
|
+
setSelectedFields([])
|
|
166
|
+
setSelectedCustomColumns([])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
32
169
|
|
|
33
170
|
const handleExport = async () => {
|
|
34
|
-
|
|
171
|
+
const trimmedFileName = fileName.trim()
|
|
172
|
+
const finalFileName = trimmedFileName || config?.defaultFileName || 'products_export.csv'
|
|
173
|
+
|
|
35
174
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
175
|
+
const serverPath = getServerLocation()
|
|
176
|
+
const endpoint = isExportAll ? 'export-all' : 'export'
|
|
177
|
+
const body = isExportAll ? undefined : JSON.stringify(productIds)
|
|
178
|
+
|
|
179
|
+
const res = await fetch(
|
|
180
|
+
`${serverPath}/product-export/${endpoint}?fileName=${encodeURIComponent(finalFileName)}&customFields=${encodeURIComponent(selectedFields.join(','))}&exportAssetsAs=${exportAssetsAs}&selectedExportFields=${encodeURIComponent([...selectedExportFields, ...selectedCustomColumns].join(','))}`,
|
|
181
|
+
{
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
...getChannelHeader(),
|
|
186
|
+
},
|
|
187
|
+
credentials: 'include',
|
|
188
|
+
...(body && { body }),
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
const json = await res.json()
|
|
194
|
+
throw new Error(json?.message || 'Export failed')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onOpenChange(false)
|
|
198
|
+
toast.success(
|
|
199
|
+
`Export job queued successfully. ${
|
|
200
|
+
isExportAll ? 'All' : productIds.length
|
|
201
|
+
} products will be exported.`,
|
|
202
|
+
)
|
|
203
|
+
} catch (error: unknown) {
|
|
204
|
+
console.error(error)
|
|
205
|
+
toast.error((error as Error)?.message || 'Failed to queue export')
|
|
39
206
|
}
|
|
40
207
|
}
|
|
41
208
|
|
|
@@ -47,21 +214,117 @@ export function ExportDialog({
|
|
|
47
214
|
<DialogDescription>Configure export settings</DialogDescription>
|
|
48
215
|
</DialogHeader>
|
|
49
216
|
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
217
|
+
<div className="space-y-4">
|
|
218
|
+
<div>
|
|
219
|
+
<Label htmlFor="file-name">File name (optional)</Label>
|
|
220
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
221
|
+
A timestamp will be automatically added to the filename to ensure uniqueness.
|
|
222
|
+
</p>
|
|
223
|
+
<Input
|
|
224
|
+
id="file-name"
|
|
225
|
+
value={fileName}
|
|
226
|
+
onChange={(e) => setFileName(e.target.value)}
|
|
227
|
+
placeholder={config?.defaultFileName || 'products_export.csv'}
|
|
228
|
+
className="mt-1"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div>
|
|
233
|
+
<Label>Select fields to export</Label>
|
|
234
|
+
<div className="flex items-center space-x-2 mt-2">
|
|
235
|
+
<Checkbox
|
|
236
|
+
id="select-all"
|
|
237
|
+
checked={toggleAllChecked}
|
|
238
|
+
onCheckedChange={(checked) => toggleSelectAll(checked as boolean)}
|
|
239
|
+
/>
|
|
240
|
+
<Label htmlFor="select-all" className="font-normal">
|
|
241
|
+
Select all
|
|
242
|
+
</Label>
|
|
243
|
+
</div>
|
|
244
|
+
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
245
|
+
{availableExportFields.map((field) => (
|
|
246
|
+
<div key={field} className="flex items-center space-x-2">
|
|
247
|
+
<Checkbox
|
|
248
|
+
id={`field-${field}`}
|
|
249
|
+
checked={selectedExportFields.includes(field)}
|
|
250
|
+
onCheckedChange={() => toggleExportFieldSelection(field)}
|
|
251
|
+
disabled={
|
|
252
|
+
config?.requiredExportFields?.includes(field) ||
|
|
253
|
+
mandatoryOptionFields.includes(field)
|
|
254
|
+
}
|
|
255
|
+
/>
|
|
256
|
+
<Label htmlFor={`field-${field}`} className="font-normal">
|
|
257
|
+
{field}
|
|
258
|
+
</Label>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
{customFields.length > 0 && (
|
|
263
|
+
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
264
|
+
{customFields.map((field) => (
|
|
265
|
+
<div key={field} className="flex items-center space-x-2">
|
|
266
|
+
<Checkbox
|
|
267
|
+
id={`custom-${field}`}
|
|
268
|
+
checked={selectedFields.includes(field)}
|
|
269
|
+
onCheckedChange={() => toggleFieldSelection(field)}
|
|
270
|
+
/>
|
|
271
|
+
<Label htmlFor={`custom-${field}`} className="font-normal">
|
|
272
|
+
{field}
|
|
273
|
+
</Label>
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{customColumnNames.length > 0 && (
|
|
279
|
+
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
280
|
+
{customColumnNames.map((name) => (
|
|
281
|
+
<div key={name} className="flex items-center space-x-2">
|
|
282
|
+
<Checkbox
|
|
283
|
+
id={`custom-column-${name}`}
|
|
284
|
+
checked={selectedCustomColumns.includes(name)}
|
|
285
|
+
onCheckedChange={() => toggleCustomColumnSelection(name)}
|
|
286
|
+
/>
|
|
287
|
+
<Label htmlFor={`custom-column-${name}`} className="font-normal">
|
|
288
|
+
{name}
|
|
289
|
+
</Label>
|
|
290
|
+
</div>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div>
|
|
297
|
+
<Label>Export assets as:</Label>
|
|
298
|
+
<RadioGroup
|
|
299
|
+
value={exportAssetsAs}
|
|
300
|
+
onValueChange={(val) => setExportAssetsAs(val as 'url' | 'json')}
|
|
301
|
+
className="mt-2"
|
|
302
|
+
>
|
|
303
|
+
{config?.exportAssetsAsOptions?.includes('url') && (
|
|
304
|
+
<div className="flex items-center space-x-2">
|
|
305
|
+
<RadioGroupItem value="url" id="assets-url" />
|
|
306
|
+
<Label htmlFor="assets-url" className="font-normal">
|
|
307
|
+
URL
|
|
308
|
+
</Label>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
{config?.exportAssetsAsOptions?.includes('json') && (
|
|
312
|
+
<div className="flex items-center space-x-2">
|
|
313
|
+
<RadioGroupItem value="json" id="assets-json" />
|
|
314
|
+
<Label htmlFor="assets-json" className="font-normal">
|
|
315
|
+
JSON
|
|
316
|
+
</Label>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</RadioGroup>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
59
322
|
|
|
60
323
|
<DialogFooter>
|
|
61
324
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
62
325
|
Cancel
|
|
63
326
|
</Button>
|
|
64
|
-
<Button type="button" onClick={handleExport}
|
|
327
|
+
<Button type="button" onClick={handleExport}>
|
|
65
328
|
Export
|
|
66
329
|
</Button>
|
|
67
330
|
</DialogFooter>
|
package/src/dashboard/export.tsx
CHANGED
|
@@ -1,60 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
CardContent,
|
|
5
|
-
CardDescription,
|
|
6
|
-
CardHeader,
|
|
7
|
-
CardTitle,
|
|
8
|
-
} from '@vendure/dashboard'
|
|
9
|
-
import { DownloadIcon, InfoIcon } from 'lucide-react'
|
|
10
|
-
import { useRef, useState } from 'react'
|
|
11
|
-
import {
|
|
12
|
-
ExportConfigurationPanel,
|
|
13
|
-
type ExportConfigurationPanelHandle,
|
|
14
|
-
} from './export-configuration-panel'
|
|
1
|
+
import { Button } from '@vendure/dashboard'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { ExportDialog } from './export-dialog'
|
|
15
4
|
|
|
16
5
|
export function ProductExportBlock() {
|
|
17
|
-
const
|
|
18
|
-
const [isExporting, setIsExporting] = useState(false)
|
|
6
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
19
7
|
|
|
20
|
-
const handleExport =
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
await panelRef.current?.submitExport()
|
|
24
|
-
} finally {
|
|
25
|
-
setIsExporting(false)
|
|
26
|
-
}
|
|
8
|
+
const handleExport = () => {
|
|
9
|
+
setDialogOpen(true)
|
|
27
10
|
}
|
|
28
11
|
|
|
29
12
|
return (
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<ExportConfigurationPanel
|
|
42
|
-
ref={panelRef}
|
|
43
|
-
isExportAll
|
|
44
|
-
active
|
|
45
|
-
idPrefix="page-export"
|
|
46
|
-
/>
|
|
47
|
-
|
|
48
|
-
<Button className="w-full" onClick={handleExport} disabled={isExporting}>
|
|
49
|
-
<DownloadIcon className="mr-2 h-4 w-4" />
|
|
50
|
-
{isExporting ? 'Queuing export…' : 'Export products'}
|
|
51
|
-
</Button>
|
|
52
|
-
|
|
53
|
-
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
|
54
|
-
<InfoIcon className="h-3.5 w-3.5 shrink-0" />
|
|
55
|
-
<span>You will receive an email when the export is ready to download.</span>
|
|
56
|
-
</div>
|
|
57
|
-
</CardContent>
|
|
58
|
-
</Card>
|
|
13
|
+
<div className="space-y-4">
|
|
14
|
+
<div>
|
|
15
|
+
<Button onClick={handleExport}>Export all products</Button>
|
|
16
|
+
</div>
|
|
17
|
+
<ExportDialog
|
|
18
|
+
open={dialogOpen}
|
|
19
|
+
onOpenChange={setDialogOpen}
|
|
20
|
+
productIds={[]}
|
|
21
|
+
isExportAll={true}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
59
24
|
)
|
|
60
25
|
}
|