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