@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +38 -11
  2. package/README.mdx +69 -59
  3. package/package.json +7 -9
  4. package/src/api/plugin.controller.d.ts +32 -2
  5. package/src/api/plugin.controller.js +30 -4
  6. package/src/api/plugin.controller.js.map +1 -1
  7. package/src/api/product-export.controller.d.ts +1 -0
  8. package/src/api/product-export.controller.js +17 -0
  9. package/src/api/product-export.controller.js.map +1 -1
  10. package/src/api/product-import.controller.js +1 -0
  11. package/src/api/product-import.controller.js.map +1 -1
  12. package/src/dashboard/bulk-export.tsx +0 -1
  13. package/src/dashboard/export-dialog.tsx +285 -22
  14. package/src/dashboard/export.tsx +17 -52
  15. package/src/dashboard/exported-list.tsx +96 -176
  16. package/src/dashboard/import.tsx +149 -286
  17. package/src/dashboard/index.tsx +28 -27
  18. package/src/dashboard/utils.ts +20 -3
  19. package/src/index.d.ts +1 -1
  20. package/src/product-import-export.plugin.d.ts +0 -6
  21. package/src/product-import-export.plugin.js +25 -29
  22. package/src/product-import-export.plugin.js.map +1 -1
  23. package/src/providers/import-providers/asset-importer.js +8 -27
  24. package/src/providers/import-providers/asset-importer.js.map +1 -1
  25. package/src/providers/import-providers/import-parser.d.ts +0 -1
  26. package/src/providers/import-providers/import-parser.js +2 -18
  27. package/src/providers/import-providers/import-parser.js.map +1 -1
  28. package/src/providers/import-providers/product-importer.d.ts +0 -10
  29. package/src/providers/import-providers/product-importer.js +34 -115
  30. package/src/providers/import-providers/product-importer.js.map +1 -1
  31. package/src/services/export-storage/export-storage-strategy.d.ts +0 -8
  32. package/src/services/extended-fast-importer.service.js +12 -20
  33. package/src/services/extended-fast-importer.service.js.map +1 -1
  34. package/src/services/import-storage/import-job-storage-strategy.d.ts +0 -9
  35. package/src/services/product-export-queue.service.js +1 -1
  36. package/src/services/product-export-queue.service.js.map +1 -1
  37. package/src/services/product-export.service.d.ts +13 -7
  38. package/src/services/product-export.service.js +96 -43
  39. package/src/services/product-export.service.js.map +1 -1
  40. package/src/types.d.ts +29 -9
  41. package/src/ui/components/product-import.component.ts +1 -0
  42. package/README.md +0 -231
  43. package/src/dashboard/export-configuration-panel.tsx +0 -404
  44. 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 { useRef, useState } from 'react'
15
+ import { useState, useEffect } from 'react'
16
+ import { toast } from 'sonner'
11
17
  import {
12
- ExportConfigurationPanel,
13
- type ExportConfigurationPanelHandle,
14
- } from './export-configuration-panel'
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
- /** Omit for export-all-only dialogs; bulk export passes selected IDs. */
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 panelRef = useRef<ExportConfigurationPanelHandle>(null)
31
- const [isSubmitting, setIsSubmitting] = useState(false)
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
- setIsSubmitting(true)
171
+ const trimmedFileName = fileName.trim()
172
+ const finalFileName = trimmedFileName || config?.defaultFileName || 'products_export.csv'
173
+
35
174
  try {
36
- await panelRef.current?.submitExport()
37
- } finally {
38
- setIsSubmitting(false)
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
- <ExportConfigurationPanel
51
- ref={panelRef}
52
- productIds={productIds}
53
- isExportAll={isExportAll}
54
- active={open}
55
- idPrefix="dialog-export"
56
- fieldsAccordionDefaultOpen
57
- onExportSuccess={() => onOpenChange(false)}
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} disabled={isSubmitting || !open}>
327
+ <Button type="button" onClick={handleExport}>
65
328
  Export
66
329
  </Button>
67
330
  </DialogFooter>
@@ -1,60 +1,25 @@
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'
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 panelRef = useRef<ExportConfigurationPanelHandle>(null)
18
- const [isExporting, setIsExporting] = useState(false)
6
+ const [dialogOpen, setDialogOpen] = useState(false)
19
7
 
20
- const handleExport = async () => {
21
- setIsExporting(true)
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
- <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>
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
  }