@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
|
@@ -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 {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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) *
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (currentPage > totalPages) setCurrentPage(1)
|
|
110
|
+
}, [totalPages, currentPage])
|
|
75
111
|
|
|
76
|
-
const
|
|
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(
|
|
134
|
-
return (bytes / (1024 * 1024)).toFixed(
|
|
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
|
-
<
|
|
147
|
-
|
|
148
|
-
<div className="space-y-
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
</
|
|
320
|
+
</Card>
|
|
241
321
|
)
|
|
242
322
|
}
|
|
243
323
|
|