@haus-tech/product-import-export-plugin 3.6.3 → 3.6.4
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 +20 -64
- package/{README.mdx → README.md} +62 -35
- package/package.json +9 -9
- package/src/dashboard/bulk-export.tsx +1 -0
- package/src/dashboard/export-configuration-panel.tsx +448 -0
- package/src/dashboard/export-dialog.tsx +22 -285
- package/src/dashboard/export.tsx +52 -17
- package/src/dashboard/exported-list.tsx +176 -96
- package/src/dashboard/import.tsx +286 -149
- package/src/dashboard/index.tsx +27 -28
- package/src/dashboard/use-active-channel-key.ts +7 -0
- package/src/product-import-export.plugin.d.ts +6 -0
- package/src/product-import-export.plugin.js +29 -1
- package/src/product-import-export.plugin.js.map +1 -1
- package/src/providers/import-providers/asset-importer.js +27 -8
- package/src/providers/import-providers/asset-importer.js.map +1 -1
- package/src/providers/import-providers/import-parser.d.ts +1 -0
- package/src/providers/import-providers/import-parser.js +18 -2
- package/src/providers/import-providers/import-parser.js.map +1 -1
- package/src/providers/import-providers/product-importer.d.ts +10 -0
- package/src/providers/import-providers/product-importer.js +111 -20
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/export-storage/export-storage-strategy.d.ts +8 -0
- package/src/services/extended-fast-importer.service.js +20 -12
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/import-storage/import-job-storage-strategy.d.ts +9 -0
- package/src/services/product-export.service.d.ts +20 -6
- package/src/services/product-export.service.js +165 -40
- package/src/services/product-export.service.js.map +1 -1
- package/src/services/self-relation-custom-fields.d.ts +26 -0
- package/src/services/self-relation-custom-fields.js +67 -0
- package/src/services/self-relation-custom-fields.js.map +1 -0
- package/src/types.d.ts +7 -0
|
@@ -6,27 +6,18 @@ import {
|
|
|
6
6
|
DialogFooter,
|
|
7
7
|
DialogHeader,
|
|
8
8
|
DialogTitle,
|
|
9
|
-
Input,
|
|
10
|
-
Label,
|
|
11
|
-
RadioGroup,
|
|
12
|
-
RadioGroupItem,
|
|
13
|
-
Checkbox,
|
|
14
9
|
} from '@vendure/dashboard'
|
|
15
|
-
import {
|
|
16
|
-
import { toast } from 'sonner'
|
|
10
|
+
import { useRef, useState } from 'react'
|
|
17
11
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type ExportFields,
|
|
22
|
-
type ProductFields,
|
|
23
|
-
type VariantFields,
|
|
24
|
-
} from './utils'
|
|
12
|
+
ExportConfigurationPanel,
|
|
13
|
+
type ExportConfigurationPanelHandle,
|
|
14
|
+
} from './export-configuration-panel'
|
|
25
15
|
|
|
26
16
|
interface ExportDialogProps {
|
|
27
17
|
open: boolean
|
|
28
18
|
onOpenChange: (open: boolean) => void
|
|
29
|
-
|
|
19
|
+
/** Omit for export-all-only dialogs; bulk export passes selected IDs. */
|
|
20
|
+
productIds?: string[]
|
|
30
21
|
isExportAll?: boolean
|
|
31
22
|
}
|
|
32
23
|
|
|
@@ -36,173 +27,15 @@ export function ExportDialog({
|
|
|
36
27
|
productIds,
|
|
37
28
|
isExportAll = false,
|
|
38
29
|
}: ExportDialogProps) {
|
|
39
|
-
const
|
|
40
|
-
const [
|
|
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
|
-
}
|
|
30
|
+
const panelRef = useRef<ExportConfigurationPanelHandle>(null)
|
|
31
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
169
32
|
|
|
170
33
|
const handleExport = async () => {
|
|
171
|
-
|
|
172
|
-
const finalFileName = trimmedFileName || config?.defaultFileName || 'products_export.csv'
|
|
173
|
-
|
|
34
|
+
setIsSubmitting(true)
|
|
174
35
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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')
|
|
36
|
+
await panelRef.current?.submitExport()
|
|
37
|
+
} finally {
|
|
38
|
+
setIsSubmitting(false)
|
|
206
39
|
}
|
|
207
40
|
}
|
|
208
41
|
|
|
@@ -214,117 +47,21 @@ export function ExportDialog({
|
|
|
214
47
|
<DialogDescription>Configure export settings</DialogDescription>
|
|
215
48
|
</DialogHeader>
|
|
216
49
|
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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>
|
|
50
|
+
<ExportConfigurationPanel
|
|
51
|
+
ref={panelRef}
|
|
52
|
+
productIds={productIds}
|
|
53
|
+
isExportAll={isExportAll}
|
|
54
|
+
active={open}
|
|
55
|
+
idPrefix="dialog-export"
|
|
56
|
+
fieldsAccordionDefaultOpen
|
|
57
|
+
onExportSuccess={() => onOpenChange(false)}
|
|
58
|
+
/>
|
|
322
59
|
|
|
323
60
|
<DialogFooter>
|
|
324
61
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
325
62
|
Cancel
|
|
326
63
|
</Button>
|
|
327
|
-
<Button type="button" onClick={handleExport}>
|
|
64
|
+
<Button type="button" onClick={handleExport} disabled={isSubmitting || !open}>
|
|
328
65
|
Export
|
|
329
66
|
</Button>
|
|
330
67
|
</DialogFooter>
|
package/src/dashboard/export.tsx
CHANGED
|
@@ -1,25 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Card,
|
|
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'
|
|
4
15
|
|
|
5
16
|
export function ProductExportBlock() {
|
|
6
|
-
const
|
|
17
|
+
const panelRef = useRef<ExportConfigurationPanelHandle>(null)
|
|
18
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
7
19
|
|
|
8
|
-
const handleExport = () => {
|
|
9
|
-
|
|
20
|
+
const handleExport = async () => {
|
|
21
|
+
setIsExporting(true)
|
|
22
|
+
try {
|
|
23
|
+
await panelRef.current?.submitExport()
|
|
24
|
+
} finally {
|
|
25
|
+
setIsExporting(false)
|
|
26
|
+
}
|
|
10
27
|
}
|
|
11
28
|
|
|
12
29
|
return (
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
<Card className="h-full">
|
|
31
|
+
<CardHeader className="flex flex-row items-start gap-3 space-y-0 items-center">
|
|
32
|
+
<div className="bg-primary/10 text-primary rounded-md p-2 flex h-9 w-9 items-center justify-center">
|
|
33
|
+
<DownloadIcon className="h-5 w-5" />
|
|
34
|
+
</div>
|
|
35
|
+
<div className="space-y-1">
|
|
36
|
+
<CardTitle className="mb-0">Export all products</CardTitle>
|
|
37
|
+
<CardDescription className="text-xs">Export all your products to a CSV file.</CardDescription>
|
|
38
|
+
</div>
|
|
39
|
+
</CardHeader>
|
|
40
|
+
<CardContent className="space-y-4">
|
|
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>
|
|
24
59
|
)
|
|
25
60
|
}
|