@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +20 -64
  2. package/{README.mdx → README.md} +62 -35
  3. package/package.json +9 -9
  4. package/src/dashboard/bulk-export.tsx +1 -0
  5. package/src/dashboard/export-configuration-panel.tsx +448 -0
  6. package/src/dashboard/export-dialog.tsx +22 -285
  7. package/src/dashboard/export.tsx +52 -17
  8. package/src/dashboard/exported-list.tsx +176 -96
  9. package/src/dashboard/import.tsx +286 -149
  10. package/src/dashboard/index.tsx +27 -28
  11. package/src/dashboard/use-active-channel-key.ts +7 -0
  12. package/src/product-import-export.plugin.d.ts +6 -0
  13. package/src/product-import-export.plugin.js +29 -1
  14. package/src/product-import-export.plugin.js.map +1 -1
  15. package/src/providers/import-providers/asset-importer.js +27 -8
  16. package/src/providers/import-providers/asset-importer.js.map +1 -1
  17. package/src/providers/import-providers/import-parser.d.ts +1 -0
  18. package/src/providers/import-providers/import-parser.js +18 -2
  19. package/src/providers/import-providers/import-parser.js.map +1 -1
  20. package/src/providers/import-providers/product-importer.d.ts +10 -0
  21. package/src/providers/import-providers/product-importer.js +111 -20
  22. package/src/providers/import-providers/product-importer.js.map +1 -1
  23. package/src/services/export-storage/export-storage-strategy.d.ts +8 -0
  24. package/src/services/extended-fast-importer.service.js +20 -12
  25. package/src/services/extended-fast-importer.service.js.map +1 -1
  26. package/src/services/import-storage/import-job-storage-strategy.d.ts +9 -0
  27. package/src/services/product-export.service.d.ts +20 -6
  28. package/src/services/product-export.service.js +165 -40
  29. package/src/services/product-export.service.js.map +1 -1
  30. package/src/services/self-relation-custom-fields.d.ts +26 -0
  31. package/src/services/self-relation-custom-fields.js +67 -0
  32. package/src/services/self-relation-custom-fields.js.map +1 -0
  33. package/src/types.d.ts +7 -0
@@ -1,16 +1,35 @@
1
1
  import {
2
2
  Button,
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
3
8
  Dialog,
4
9
  DialogContent,
5
10
  DialogDescription,
6
11
  DialogFooter,
7
12
  DialogHeader,
8
13
  DialogTitle,
14
+ Input,
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ toast,
9
22
  } from '@vendure/dashboard'
10
- import { getServerLocation, getChannelHeader } from './utils'
11
- import { useState, useEffect, useMemo } from 'react'
12
- import { DownloadIcon, ChevronLeftIcon, ChevronRightIcon, TrashIcon } from 'lucide-react'
13
- import { toast } from 'sonner'
23
+ import {
24
+ ChevronLeftIcon,
25
+ ChevronRightIcon,
26
+ DownloadIcon,
27
+ SearchIcon,
28
+ TrashIcon,
29
+ } from 'lucide-react'
30
+ import { useEffect, useMemo, useState } from 'react'
31
+ import { useActiveChannelKey } from './use-active-channel-key'
32
+ import { getChannelHeader, getServerLocation } from './utils'
14
33
 
15
34
  interface ExportedFile {
16
35
  fileName: string
@@ -18,12 +37,14 @@ interface ExportedFile {
18
37
  created: string
19
38
  }
20
39
 
21
- const itemsPerPage = 10
40
+ const ITEMS_PER_PAGE = 8
22
41
  const POLL_INTERVAL_MS = 10_000
23
42
 
24
43
  export function ExportedList() {
44
+ const activeChannelId = useActiveChannelKey()
25
45
  const [exportedFiles, setExportedFiles] = useState<ExportedFile[]>([])
26
46
  const [error, setError] = useState<string | null>(null)
47
+ const [search, setSearch] = useState('')
27
48
  const [currentPage, setCurrentPage] = useState(1)
28
49
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
29
50
  const [fileToDelete, setFileToDelete] = useState<string | null>(null)
@@ -53,27 +74,42 @@ export function ExportedList() {
53
74
  }
54
75
 
55
76
  useEffect(() => {
77
+ if (!activeChannelId) return
78
+ setSearch('')
79
+ setCurrentPage(1)
56
80
  void getExportFiles()
57
- }, [])
81
+ }, [activeChannelId])
58
82
 
59
83
  useEffect(() => {
84
+ if (!activeChannelId) return
85
+
60
86
  const intervalId = setInterval(() => {
61
87
  void getExportFiles()
62
88
  }, POLL_INTERVAL_MS)
63
89
 
64
90
  return () => clearInterval(intervalId)
65
- }, [])
91
+ }, [activeChannelId])
66
92
 
67
- // Pagination calculations
93
+ const filteredFiles = useMemo(() => {
94
+ let result = exportedFiles
95
+ if (search) {
96
+ const q = search.toLowerCase()
97
+ result = result.filter((f) => f.fileName.toLowerCase().includes(q))
98
+ }
99
+ return result
100
+ }, [exportedFiles, search])
101
+
102
+ const totalPages = Math.max(1, Math.ceil(filteredFiles.length / ITEMS_PER_PAGE))
68
103
  const paginatedFiles = useMemo(() => {
69
- const startIndex = (currentPage - 1) * itemsPerPage
70
- const endIndex = startIndex + itemsPerPage
71
- return exportedFiles.slice(startIndex, endIndex)
72
- }, [exportedFiles, currentPage])
104
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
105
+ return filteredFiles.slice(startIndex, startIndex + ITEMS_PER_PAGE)
106
+ }, [filteredFiles, currentPage])
73
107
 
74
- const totalPages = Math.ceil(exportedFiles.length / itemsPerPage)
108
+ useEffect(() => {
109
+ if (currentPage > totalPages) setCurrentPage(1)
110
+ }, [totalPages, currentPage])
75
111
 
76
- const downLoadFile = async (fileName: string) => {
112
+ const downloadFile = async (fileName: string) => {
77
113
  const serverPath = getServerLocation()
78
114
  const res = await fetch(`${serverPath}/product-export/download/${fileName}`, {
79
115
  method: 'GET',
@@ -130,94 +166,138 @@ export function ExportedList() {
130
166
 
131
167
  const formatFileSize = (bytes: number): string => {
132
168
  if (bytes < 1024) return bytes + ' B'
133
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
134
- return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
135
- }
136
-
137
- if (error) {
138
- return (
139
- <div className="text-center text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed">
140
- <p className="text-red-500">{error}</p>
141
- </div>
142
- )
169
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
170
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
143
171
  }
144
172
 
145
173
  return (
146
- <div>
147
- {exportedFiles.length > 0 && (
148
- <div className="space-y-2">
149
- <div className="border rounded-md divide-y">
150
- {paginatedFiles.map((file) => (
151
- <div key={file.fileName} className="p-4 hover:bg-muted/50 transition-colors">
152
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
153
- <div className="flex-1">
154
- <p className="font-medium">{file.fileName}</p>
155
- <div className="flex flex-col sm:flex-row gap-2 sm:gap-4 text-sm text-muted-foreground mt-1">
156
- <span>Size: {formatFileSize(file.size)}</span>
157
- <span>
158
- Created:{' '}
159
- {new Date(file.created).toLocaleString(navigator.language || undefined)}
160
- </span>
161
- </div>
162
- </div>
163
- <div className="flex items-center gap-2">
164
- <Button variant="outline" size="sm" onClick={() => downLoadFile(file.fileName)}>
165
- <DownloadIcon className="mr-2 h-4 w-4" />
166
- Download
167
- </Button>
168
- <Button
169
- variant="outline"
170
- size="sm"
171
- onClick={() => handleDeleteClick(file.fileName)}
172
- className="text-destructive hover:text-destructive"
173
- >
174
- <TrashIcon className="h-4 w-4" /> Delete
175
- </Button>
176
- </div>
177
- </div>
178
- </div>
179
- ))}
174
+ <Card>
175
+ <CardHeader className="flex flex-col gap-4 space-y-0 pb-4 md:flex-row md:items-start md:justify-between md:gap-6">
176
+ <div className="min-w-0 space-y-1 md:pr-4">
177
+ <CardTitle className="mb-0">Recent exports</CardTitle>
178
+ <CardDescription className=" text-xs">
179
+ Completed CSV exports for this channel. Search by file name to filter the list.
180
+ </CardDescription>
181
+ </div>
182
+ <div className="relative w-full shrink-0 md:w-72 lg:w-80">
183
+ <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
184
+ <Input
185
+ type="search"
186
+ placeholder="Search by file name…"
187
+ value={search}
188
+ onChange={(e) => setSearch(e.target.value)}
189
+ className="pl-8"
190
+ aria-label="Search exports by file name"
191
+ />
192
+ </div>
193
+ </CardHeader>
194
+ <CardContent>
195
+ {error ? (
196
+ <div className="text-sm text-destructive py-4">{error}</div>
197
+ ) : exportedFiles.length === 0 ? (
198
+ <div className="text-center py-10 text-sm text-muted-foreground">
199
+ <p>No exports yet. Run an export to see files listed here.</p>
180
200
  </div>
201
+ ) : (
202
+ <>
203
+ <div className="rounded-md border overflow-hidden">
204
+ <Table>
205
+ <TableHeader>
206
+ <TableRow>
207
+ <TableHead className="w-[140px]">Type</TableHead>
208
+ <TableHead>File</TableHead>
209
+ <TableHead>Created</TableHead>
210
+ <TableHead className="text-right">Actions</TableHead>
211
+ </TableRow>
212
+ </TableHeader>
213
+ <TableBody>
214
+ {paginatedFiles.length === 0 ? (
215
+ <TableRow>
216
+ <TableCell colSpan={4} className="text-center text-muted-foreground py-8">
217
+ No results match your filters.
218
+ </TableCell>
219
+ </TableRow>
220
+ ) : (
221
+ paginatedFiles.map((file) => (
222
+ <TableRow key={file.fileName}>
223
+ <TableCell>
224
+ <div className="flex items-center gap-2">
225
+ <span className="bg-primary/10 text-primary flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
226
+ <DownloadIcon className="h-4 w-4" aria-hidden />
227
+ </span>
228
+ <span className="text-sm font-medium">Export</span>
229
+ </div>
230
+ </TableCell>
231
+ <TableCell>
232
+ <div className="font-medium truncate max-w-xs">{file.fileName}</div>
233
+ <div className="text-xs text-muted-foreground">
234
+ {formatFileSize(file.size)}
235
+ </div>
236
+ </TableCell>
237
+ <TableCell>
238
+ <span className="text-sm">
239
+ {new Date(file.created).toLocaleString(navigator.language || undefined)}
240
+ </span>
241
+ </TableCell>
242
+ <TableCell className="text-right">
243
+ <div className="inline-flex gap-2">
244
+ <Button
245
+ variant="outline"
246
+ size="sm"
247
+ onClick={() => downloadFile(file.fileName)}
248
+ >
249
+ <DownloadIcon className="mr-1 h-3.5 w-3.5" /> Download
250
+ </Button>
251
+ <Button
252
+ variant="ghost"
253
+ size="sm"
254
+ onClick={() => handleDeleteClick(file.fileName)}
255
+ className="text-muted-foreground hover:text-destructive"
256
+ >
257
+ <TrashIcon className="h-3.5 w-3.5" />
258
+ </Button>
259
+ </div>
260
+ </TableCell>
261
+ </TableRow>
262
+ ))
263
+ )}
264
+ </TableBody>
265
+ </Table>
266
+ </div>
181
267
 
182
- {/* Pagination controls */}
183
- {totalPages > 1 && (
184
- <div className="flex items-center justify-between py-2 border-t">
185
- <div className="text-sm text-muted-foreground">
186
- Showing {(currentPage - 1) * itemsPerPage + 1} to{' '}
187
- {Math.min(currentPage * itemsPerPage, exportedFiles.length)} of{' '}
188
- {exportedFiles.length}
189
- </div>
190
- <div className="flex items-center gap-1">
191
- <Button
192
- variant="outline"
193
- size="sm"
194
- onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
195
- disabled={currentPage === 1}
196
- >
197
- <ChevronLeftIcon className="h-4 w-4" />
198
- </Button>
199
- <div className="text-sm text-muted-foreground px-2">
200
- {currentPage} / {totalPages}
268
+ {filteredFiles.length > ITEMS_PER_PAGE && (
269
+ <div className="flex items-center justify-between mt-4">
270
+ <p className="text-xs text-muted-foreground">
271
+ Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1}–
272
+ {Math.min(currentPage * ITEMS_PER_PAGE, filteredFiles.length)} of{' '}
273
+ {filteredFiles.length}
274
+ </p>
275
+ <div className="flex items-center gap-1">
276
+ <Button
277
+ variant="outline"
278
+ size="sm"
279
+ onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
280
+ disabled={currentPage === 1}
281
+ >
282
+ <ChevronLeftIcon className="h-4 w-4" />
283
+ </Button>
284
+ <span className="text-sm px-2 text-muted-foreground">
285
+ {currentPage} / {totalPages}
286
+ </span>
287
+ <Button
288
+ variant="outline"
289
+ size="sm"
290
+ onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
291
+ disabled={currentPage === totalPages}
292
+ >
293
+ <ChevronRightIcon className="h-4 w-4" />
294
+ </Button>
201
295
  </div>
202
- <Button
203
- variant="outline"
204
- size="sm"
205
- onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
206
- disabled={currentPage === totalPages}
207
- >
208
- <ChevronRightIcon className="h-4 w-4" />
209
- </Button>
210
296
  </div>
211
- </div>
212
- )}
213
- </div>
214
- )}
215
-
216
- {exportedFiles.length === 0 && (
217
- <div className="text-center py-8 text-muted-foreground text-sm">
218
- <p>You have no exported files</p>
219
- </div>
220
- )}
297
+ )}
298
+ </>
299
+ )}
300
+ </CardContent>
221
301
 
222
302
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
223
303
  <DialogContent>
@@ -237,7 +317,7 @@ export function ExportedList() {
237
317
  </DialogFooter>
238
318
  </DialogContent>
239
319
  </Dialog>
240
- </div>
320
+ </Card>
241
321
  )
242
322
  }
243
323