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