@haus-tech/product-import-export-plugin 3.3.10-next.0 → 3.3.10-next.2
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 +0 -24
- package/README.mdx +30 -56
- package/package.json +5 -5
- package/src/dashboard/bulk-export.tsx +0 -1
- package/src/dashboard/export-dialog.tsx +252 -22
- package/src/dashboard/export.tsx +17 -52
- package/src/dashboard/exported-list.tsx +103 -167
- package/src/dashboard/import.tsx +148 -258
- package/src/dashboard/index.tsx +28 -27
- package/src/providers/import-providers/asset-importer.js +8 -27
- package/src/providers/import-providers/asset-importer.js.map +1 -1
- package/src/providers/import-providers/import-parser.d.ts +0 -1
- package/src/providers/import-providers/import-parser.js +2 -18
- package/src/providers/import-providers/import-parser.js.map +1 -1
- package/src/providers/import-providers/product-importer.d.ts +1 -10
- package/src/providers/import-providers/product-importer.js +39 -132
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/extended-fast-importer.service.js +12 -20
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/product-export.service.d.ts +5 -11
- package/src/services/product-export.service.js +28 -56
- package/src/services/product-export.service.js.map +1 -1
- package/src/dashboard/export-configuration-panel.tsx +0 -418
package/src/dashboard/import.tsx
CHANGED
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
Card,
|
|
4
|
-
CardContent,
|
|
5
|
-
CardDescription,
|
|
6
|
-
CardHeader,
|
|
7
|
-
CardTitle,
|
|
8
|
-
Switch,
|
|
9
|
-
Label,
|
|
10
|
-
RadioGroup,
|
|
11
|
-
RadioGroupItem,
|
|
12
|
-
Dialog,
|
|
13
|
-
DialogContent,
|
|
14
|
-
DialogDescription,
|
|
15
|
-
DialogFooter,
|
|
16
|
-
DialogHeader,
|
|
17
|
-
DialogTitle,
|
|
18
|
-
toast,
|
|
19
|
-
} from '@vendure/dashboard'
|
|
20
|
-
import {
|
|
21
|
-
UploadIcon,
|
|
22
|
-
UploadCloudIcon,
|
|
23
|
-
FileTextIcon,
|
|
24
|
-
XIcon,
|
|
25
|
-
DownloadIcon,
|
|
26
|
-
ExternalLinkIcon,
|
|
27
|
-
} from 'lucide-react'
|
|
1
|
+
import { Button, Switch, Label, RadioGroup, RadioGroupItem } from '@vendure/dashboard'
|
|
2
|
+
import { UploadIcon } from 'lucide-react'
|
|
28
3
|
import { useState, useRef, useEffect } from 'react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
29
5
|
import type { LanguageCode, Channel } from '@vendure/core'
|
|
30
6
|
import { size, startsWith, endsWith, uniq } from 'lodash-es'
|
|
31
7
|
import {
|
|
@@ -36,26 +12,17 @@ import {
|
|
|
36
12
|
type PluginInitOptions,
|
|
37
13
|
} from './utils'
|
|
38
14
|
|
|
39
|
-
|
|
40
|
-
'https://github.com/WeAreHausTech/haus-tech-vendure-plugins-public/blob/main/packages/product-import-export-plugin/README.mdx#csv-format-for-import'
|
|
41
|
-
const GUIDE_URL =
|
|
42
|
-
'https://github.com/WeAreHausTech/haus-tech-vendure-plugins-public/blob/main/packages/product-import-export-plugin/README.mdx'
|
|
43
|
-
|
|
44
|
-
const MAX_SIZE_BYTES = 20 * 1024 * 1024
|
|
45
|
-
|
|
15
|
+
// Import page component
|
|
46
16
|
export function ProductImportBlock() {
|
|
47
17
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
48
18
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
49
19
|
const [validateFile, setValidateFile] = useState<File | null>(null)
|
|
50
|
-
const [isDragActive, setIsDragActive] = useState(false)
|
|
51
|
-
const [optionsOpen, setOptionsOpen] = useState(false)
|
|
52
20
|
const [updateProductSlug, setUpdateProductSlug] = useState(true)
|
|
53
21
|
const [selectedMainLanguage, setSelectedMainLanguage] = useState<LanguageCode | undefined>()
|
|
54
22
|
const [availableLanguages, setAvailableLanguages] = useState<LanguageCode[]>([])
|
|
55
23
|
const [updatingStrategy, setUpdatingStrategy] = useState<UpdatingStrategy>('merge')
|
|
56
24
|
const [config, setConfig] = useState<PluginInitOptions | null>(null)
|
|
57
25
|
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null)
|
|
58
|
-
const [isUploading, setIsUploading] = useState(false)
|
|
59
26
|
|
|
60
27
|
useEffect(() => {
|
|
61
28
|
const fetchConfig = async () => {
|
|
@@ -108,6 +75,7 @@ export function ProductImportBlock() {
|
|
|
108
75
|
endChar: string
|
|
109
76
|
isJson?: boolean
|
|
110
77
|
} => {
|
|
78
|
+
// Replace starting and ending double quotes if exists
|
|
111
79
|
const trimmedText = text.trim().replace(/^"/, '').replace(/"$/, '')
|
|
112
80
|
|
|
113
81
|
return {
|
|
@@ -150,6 +118,7 @@ export function ProductImportBlock() {
|
|
|
150
118
|
}
|
|
151
119
|
for (const lang of languageCodes) {
|
|
152
120
|
if (lang) {
|
|
121
|
+
// Check if lang is defined
|
|
153
122
|
const langSpecificColumn = `${baseColumn}:${lang}`
|
|
154
123
|
const langColIndex = header.indexOf(langSpecificColumn)
|
|
155
124
|
const colValue = row[langColIndex]
|
|
@@ -204,6 +173,7 @@ export function ProductImportBlock() {
|
|
|
204
173
|
|
|
205
174
|
const header = splitLines(lines[0]).map((col) => col.trim())
|
|
206
175
|
|
|
176
|
+
// Fetch channel if not already loaded (matches Angular implementation)
|
|
207
177
|
let channel = currentChannel
|
|
208
178
|
if (!channel) {
|
|
209
179
|
try {
|
|
@@ -217,6 +187,7 @@ export function ProductImportBlock() {
|
|
|
217
187
|
credentials: 'include',
|
|
218
188
|
})
|
|
219
189
|
channel = await channelRes.json()
|
|
190
|
+
// Update state for next time
|
|
220
191
|
setCurrentChannel(channel)
|
|
221
192
|
} catch (error) {
|
|
222
193
|
console.error('Failed to fetch channel:', error)
|
|
@@ -244,6 +215,7 @@ export function ProductImportBlock() {
|
|
|
244
215
|
}
|
|
245
216
|
|
|
246
217
|
if (!selectedMainLanguage && languageCodes.length === 0) {
|
|
218
|
+
// No language codes in CSV, return channel languages so user can select
|
|
247
219
|
resolve({
|
|
248
220
|
isValid: true,
|
|
249
221
|
langCodes: availableLanguages.map((lang) => lang) as (string | undefined)[],
|
|
@@ -280,6 +252,7 @@ export function ProductImportBlock() {
|
|
|
280
252
|
|
|
281
253
|
const nameColumn = validationLanguage ? `name:${validationLanguage}` : 'name'
|
|
282
254
|
|
|
255
|
+
// Get optionGroups and optionValues columns for the main language
|
|
283
256
|
const optionGroupsColumn = validationLanguage
|
|
284
257
|
? `optionGroups:${validationLanguage}`
|
|
285
258
|
: 'optionGroups'
|
|
@@ -290,13 +263,17 @@ export function ProductImportBlock() {
|
|
|
290
263
|
const optionGroupsIndex = header.indexOf(optionGroupsColumn)
|
|
291
264
|
const optionValuesIndex = header.indexOf(optionValuesColumn)
|
|
292
265
|
|
|
266
|
+
// Loop through each row to validate the option values
|
|
293
267
|
let currentOptionGroupCount = 0
|
|
294
268
|
|
|
295
269
|
for (let i = 1; i < lines.length; i++) {
|
|
296
270
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
271
|
+
|
|
272
|
+
// Check if it's a product row (it has a 'name' field)
|
|
297
273
|
const isProductRow = row[header.indexOf(nameColumn)]
|
|
298
274
|
|
|
299
275
|
if (isProductRow) {
|
|
276
|
+
// It's a new product row, get the option groups
|
|
300
277
|
const optionGroups = (row[optionGroupsIndex]?.split('|') ?? []).filter(
|
|
301
278
|
(group) => group !== '',
|
|
302
279
|
)
|
|
@@ -305,6 +282,7 @@ export function ProductImportBlock() {
|
|
|
305
282
|
)
|
|
306
283
|
currentOptionGroupCount = optionGroups.length
|
|
307
284
|
|
|
285
|
+
// Validate that the number of option values matches the number of option groups
|
|
308
286
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
309
287
|
toast.error(
|
|
310
288
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -313,10 +291,12 @@ export function ProductImportBlock() {
|
|
|
313
291
|
return
|
|
314
292
|
}
|
|
315
293
|
} else {
|
|
294
|
+
// It's a variant row, validate the option values
|
|
316
295
|
const optionValues = (row[optionValuesIndex]?.split('|') ?? []).filter(
|
|
317
296
|
(value) => value !== '',
|
|
318
297
|
)
|
|
319
298
|
|
|
299
|
+
// Validate that the number of option values matches the product's option groups
|
|
320
300
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
321
301
|
toast.error(
|
|
322
302
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -327,6 +307,7 @@ export function ProductImportBlock() {
|
|
|
327
307
|
}
|
|
328
308
|
}
|
|
329
309
|
|
|
310
|
+
// Check if a row contains JSON, if so check if it is valid. This should be done for all columns, not just translatable ones.
|
|
330
311
|
for (let i = 1; i < lines.length; i++) {
|
|
331
312
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
332
313
|
for (const col of header) {
|
|
@@ -357,6 +338,7 @@ export function ProductImportBlock() {
|
|
|
357
338
|
}
|
|
358
339
|
}
|
|
359
340
|
|
|
341
|
+
// Check for missing translation values in translatable base columns
|
|
360
342
|
const { errorRows: missingBaseTranslatableValuesRows } = checkTranslatabeColumns(
|
|
361
343
|
lines,
|
|
362
344
|
translatableBaseColumns,
|
|
@@ -373,6 +355,7 @@ export function ProductImportBlock() {
|
|
|
373
355
|
return
|
|
374
356
|
}
|
|
375
357
|
|
|
358
|
+
// Check if all translatable columns have values for the main language
|
|
376
359
|
const { errorRows, errorColumns } = checkTranslatabeColumns(
|
|
377
360
|
lines,
|
|
378
361
|
translatableColumns,
|
|
@@ -404,15 +387,15 @@ export function ProductImportBlock() {
|
|
|
404
387
|
const { isValid, langCodes, clearFile = true } = await validateCsvStructure(file)
|
|
405
388
|
if (isValid) {
|
|
406
389
|
setSelectedFile(file)
|
|
407
|
-
setOptionsOpen(true)
|
|
408
390
|
} else if (clearFile) {
|
|
409
391
|
clearFileHandler()
|
|
410
392
|
return
|
|
411
393
|
} else {
|
|
394
|
+
// File is valid but needs language selection
|
|
412
395
|
setSelectedFile(file)
|
|
413
|
-
setOptionsOpen(true)
|
|
414
396
|
}
|
|
415
397
|
|
|
398
|
+
// Set available languages - either from CSV or from channel
|
|
416
399
|
if (langCodes && langCodes.length > 0) {
|
|
417
400
|
const filteredLangCodes = langCodes.filter((code) => code) as LanguageCode[]
|
|
418
401
|
setAvailableLanguages(filteredLangCodes)
|
|
@@ -423,6 +406,7 @@ export function ProductImportBlock() {
|
|
|
423
406
|
currentChannel?.availableLanguageCodes &&
|
|
424
407
|
currentChannel.availableLanguageCodes.length > 0
|
|
425
408
|
) {
|
|
409
|
+
// No languages in CSV headers, use channel languages as fallback
|
|
426
410
|
setAvailableLanguages(currentChannel.availableLanguageCodes)
|
|
427
411
|
if (!selectedMainLanguage) {
|
|
428
412
|
setSelectedMainLanguage(
|
|
@@ -434,35 +418,19 @@ export function ProductImportBlock() {
|
|
|
434
418
|
}
|
|
435
419
|
}
|
|
436
420
|
|
|
437
|
-
const handleFile = (file: File) => {
|
|
438
|
-
if (file.type !== 'text/csv' && !file.name.toLowerCase().endsWith('.csv')) {
|
|
439
|
-
toast.error('Invalid file type. Please upload a CSV file.')
|
|
440
|
-
clearFileHandler()
|
|
441
|
-
return
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (file.size > MAX_SIZE_BYTES) {
|
|
445
|
-
toast.error('File is larger than 20MB.')
|
|
446
|
-
clearFileHandler()
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
setValidateFile(file)
|
|
451
|
-
doValidation(file)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
421
|
const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
455
422
|
const input = event.target
|
|
456
423
|
if (input.files && input.files.length > 0) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
424
|
+
const file = input.files[0]
|
|
425
|
+
if (file.type !== 'text/csv') {
|
|
426
|
+
toast.error('Invalid file type. Please upload a CSV file.')
|
|
427
|
+
clearFileHandler()
|
|
428
|
+
return
|
|
429
|
+
}
|
|
460
430
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const file = event.dataTransfer.files?.[0]
|
|
465
|
-
if (file) handleFile(file)
|
|
431
|
+
setValidateFile(file)
|
|
432
|
+
doValidation(file)
|
|
433
|
+
}
|
|
466
434
|
}
|
|
467
435
|
|
|
468
436
|
const clearFileHandler = () => {
|
|
@@ -473,7 +441,6 @@ export function ProductImportBlock() {
|
|
|
473
441
|
setValidateFile(null)
|
|
474
442
|
setSelectedMainLanguage(undefined)
|
|
475
443
|
setUpdatingStrategy('merge')
|
|
476
|
-
setOptionsOpen(false)
|
|
477
444
|
}
|
|
478
445
|
|
|
479
446
|
const onMainLanguageChange = (lang: LanguageCode) => {
|
|
@@ -484,214 +451,137 @@ export function ProductImportBlock() {
|
|
|
484
451
|
}
|
|
485
452
|
|
|
486
453
|
const uploadFile = async () => {
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
454
|
+
if (selectedFile && selectedMainLanguage && updatingStrategy) {
|
|
455
|
+
const formData = new FormData()
|
|
456
|
+
formData.append('file', selectedFile)
|
|
457
|
+
formData.append('updateProductSlug', updateProductSlug.toString())
|
|
458
|
+
formData.append('mainLanguage', selectedMainLanguage)
|
|
459
|
+
formData.append('updatingStrategy', updatingStrategy)
|
|
491
460
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
clearFileHandler()
|
|
512
|
-
toast.success('The file was uploaded and has been added to the import queue.')
|
|
513
|
-
} else {
|
|
461
|
+
try {
|
|
462
|
+
const serverPath = getServerLocation()
|
|
463
|
+
const res = await fetch(`${serverPath}/product-import/upload`, {
|
|
464
|
+
method: 'POST',
|
|
465
|
+
body: formData,
|
|
466
|
+
headers: {
|
|
467
|
+
...getChannelHeader(),
|
|
468
|
+
},
|
|
469
|
+
credentials: 'include',
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
if (res.ok) {
|
|
473
|
+
clearFileHandler()
|
|
474
|
+
toast.success('The file was uploaded and has been added to the import queue.')
|
|
475
|
+
} else {
|
|
476
|
+
toast.error('An error occurred while uploading the file.')
|
|
477
|
+
}
|
|
478
|
+
} catch (error: unknown) {
|
|
479
|
+
console.error('Upload error:', error)
|
|
514
480
|
toast.error('An error occurred while uploading the file.')
|
|
515
481
|
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
toast.error('An error occurred while uploading the file.')
|
|
519
|
-
} finally {
|
|
520
|
-
setIsUploading(false)
|
|
482
|
+
} else {
|
|
483
|
+
toast.error('No file selected or main language not selected.')
|
|
521
484
|
}
|
|
522
485
|
}
|
|
523
486
|
|
|
524
487
|
return (
|
|
525
|
-
|
|
526
|
-
<
|
|
527
|
-
<
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
<XIcon className="mr-1 h-3 w-3" /> Remove
|
|
582
|
-
</Button>
|
|
583
|
-
</div>
|
|
584
|
-
) : (
|
|
585
|
-
<div className="flex flex-col items-center gap-3">
|
|
586
|
-
<UploadCloudIcon className="h-10 w-10 text-muted-foreground" />
|
|
587
|
-
<div className="space-y-1">
|
|
588
|
-
<p className="text-sm font-medium">Drag & drop your CSV file here</p>
|
|
589
|
-
<p className="text-xs text-muted-foreground">or</p>
|
|
590
|
-
</div>
|
|
591
|
-
<Button type="button" variant="outline" size="sm">
|
|
592
|
-
Choose file
|
|
593
|
-
</Button>
|
|
594
|
-
<p className="text-xs text-muted-foreground">
|
|
595
|
-
CSV UTF-8 only • Max file size: 20MB
|
|
596
|
-
</p>
|
|
488
|
+
<div className="space-y-6">
|
|
489
|
+
<div>
|
|
490
|
+
<Label htmlFor="file-upload">Upload CSV file</Label>
|
|
491
|
+
<div className="flex items-center gap-2 mt-2">
|
|
492
|
+
<input
|
|
493
|
+
ref={fileInputRef}
|
|
494
|
+
type="file"
|
|
495
|
+
id="file-upload"
|
|
496
|
+
accept=".csv"
|
|
497
|
+
onChange={onFileSelected}
|
|
498
|
+
className="hidden"
|
|
499
|
+
/>
|
|
500
|
+
{selectedFile ? (
|
|
501
|
+
<>
|
|
502
|
+
<Button type="button" variant="outline" onClick={() => fileInputRef.current?.click()}>
|
|
503
|
+
<UploadIcon className="mr-2 h-4 w-4" />
|
|
504
|
+
{selectedFile.name}
|
|
505
|
+
</Button>
|
|
506
|
+
</>
|
|
507
|
+
) : (
|
|
508
|
+
<Button type="button" variant="default" onClick={() => fileInputRef.current?.click()}>
|
|
509
|
+
<UploadIcon className="mr-2 h-4 w-4" />
|
|
510
|
+
Select file
|
|
511
|
+
</Button>
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
{selectedFile && (
|
|
517
|
+
<div className="space-y-4 pt-4 border-t">
|
|
518
|
+
{config?.visibleOptions?.includes('updateProductSlug') && (
|
|
519
|
+
<div className="flex items-center space-x-2">
|
|
520
|
+
<Switch
|
|
521
|
+
id="update-slug"
|
|
522
|
+
checked={updateProductSlug}
|
|
523
|
+
onCheckedChange={setUpdateProductSlug}
|
|
524
|
+
/>
|
|
525
|
+
<Label htmlFor="update-slug">Update product slugs based on name</Label>
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
528
|
+
|
|
529
|
+
<div>
|
|
530
|
+
<Label>Select main language:</Label>
|
|
531
|
+
<RadioGroup
|
|
532
|
+
value={selectedMainLanguage || ''}
|
|
533
|
+
onValueChange={(val) => onMainLanguageChange(val as LanguageCode)}
|
|
534
|
+
>
|
|
535
|
+
<div className="flex flex-col gap-2 mt-2">
|
|
536
|
+
{availableLanguages.map((lang) => (
|
|
537
|
+
<div key={lang} className="flex items-center space-x-2">
|
|
538
|
+
<RadioGroupItem value={lang} id={`lang-${lang}`} />
|
|
539
|
+
<Label htmlFor={`lang-${lang}`} className="font-normal">
|
|
540
|
+
{lang}
|
|
541
|
+
</Label>
|
|
542
|
+
</div>
|
|
543
|
+
))}
|
|
597
544
|
</div>
|
|
598
|
-
|
|
545
|
+
</RadioGroup>
|
|
546
|
+
<p className="text-sm text-muted-foreground mt-2 italic">
|
|
547
|
+
The main language is used as a fallback when translations are missing. It is
|
|
548
|
+
recommended to select the language that is used the most in your system.
|
|
549
|
+
</p>
|
|
599
550
|
</div>
|
|
600
|
-
</CardContent>
|
|
601
|
-
</Card>
|
|
602
|
-
|
|
603
|
-
<Dialog open={optionsOpen} onOpenChange={(open) => (open ? setOptionsOpen(true) : clearFileHandler())}>
|
|
604
|
-
<DialogContent className="max-w-lg">
|
|
605
|
-
<DialogHeader>
|
|
606
|
-
<DialogTitle>Import options</DialogTitle>
|
|
607
|
-
<DialogDescription>
|
|
608
|
-
Configure how this CSV should be imported into your catalog.
|
|
609
|
-
</DialogDescription>
|
|
610
|
-
</DialogHeader>
|
|
611
|
-
|
|
612
|
-
<div className="space-y-4">
|
|
613
|
-
{selectedFile && (
|
|
614
|
-
<div className="bg-muted/40 rounded-md px-3 py-2 text-sm flex items-center gap-2">
|
|
615
|
-
<FileTextIcon className="h-4 w-4 text-primary" />
|
|
616
|
-
<span className="font-medium truncate">{selectedFile.name}</span>
|
|
617
|
-
</div>
|
|
618
|
-
)}
|
|
619
551
|
|
|
620
|
-
|
|
552
|
+
<div>
|
|
553
|
+
<Label>Strategy for facets and assets:</Label>
|
|
554
|
+
<RadioGroup
|
|
555
|
+
value={updatingStrategy}
|
|
556
|
+
onValueChange={(val) => setUpdatingStrategy(val as UpdatingStrategy)}
|
|
557
|
+
>
|
|
558
|
+
<div className="flex items-center space-x-2 mt-2">
|
|
559
|
+
<RadioGroupItem value="merge" id="merge" />
|
|
560
|
+
<Label htmlFor="merge" className="font-normal">
|
|
561
|
+
Merge
|
|
562
|
+
</Label>
|
|
563
|
+
</div>
|
|
621
564
|
<div className="flex items-center space-x-2">
|
|
622
|
-
<
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
/>
|
|
627
|
-
<Label htmlFor="update-slug">Update product slugs based on name</Label>
|
|
565
|
+
<RadioGroupItem value="replace" id="replace" />
|
|
566
|
+
<Label htmlFor="replace" className="font-normal">
|
|
567
|
+
Overwrite
|
|
568
|
+
</Label>
|
|
628
569
|
</div>
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
<RadioGroup
|
|
634
|
-
value={selectedMainLanguage || ''}
|
|
635
|
-
onValueChange={(val) => onMainLanguageChange(val as LanguageCode)}
|
|
636
|
-
>
|
|
637
|
-
<div className="flex flex-wrap gap-3 mt-2">
|
|
638
|
-
{availableLanguages.map((lang) => (
|
|
639
|
-
<div key={lang} className="flex items-center space-x-2">
|
|
640
|
-
<RadioGroupItem value={lang} id={`lang-${lang}`} />
|
|
641
|
-
<Label htmlFor={`lang-${lang}`} className="font-normal uppercase">
|
|
642
|
-
{lang}
|
|
643
|
-
</Label>
|
|
644
|
-
</div>
|
|
645
|
-
))}
|
|
646
|
-
</div>
|
|
647
|
-
</RadioGroup>
|
|
648
|
-
<p className="text-xs text-muted-foreground mt-2">
|
|
649
|
-
Used as a fallback when a translation is missing.
|
|
570
|
+
</RadioGroup>
|
|
571
|
+
{updatingStrategy === 'replace' && (
|
|
572
|
+
<p className="text-sm text-red-600 mt-2">
|
|
573
|
+
Warning: This will remove existing facets and assets from products and variants.
|
|
650
574
|
</p>
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
<div>
|
|
654
|
-
<Label>Strategy for facets and assets</Label>
|
|
655
|
-
<RadioGroup
|
|
656
|
-
value={updatingStrategy}
|
|
657
|
-
onValueChange={(val) => setUpdatingStrategy(val as UpdatingStrategy)}
|
|
658
|
-
className="mt-2"
|
|
659
|
-
>
|
|
660
|
-
<div className="flex items-center space-x-2">
|
|
661
|
-
<RadioGroupItem value="merge" id="merge" />
|
|
662
|
-
<Label htmlFor="merge" className="font-normal">
|
|
663
|
-
Merge
|
|
664
|
-
</Label>
|
|
665
|
-
</div>
|
|
666
|
-
<div className="flex items-center space-x-2">
|
|
667
|
-
<RadioGroupItem value="replace" id="replace" />
|
|
668
|
-
<Label htmlFor="replace" className="font-normal">
|
|
669
|
-
Overwrite
|
|
670
|
-
</Label>
|
|
671
|
-
</div>
|
|
672
|
-
</RadioGroup>
|
|
673
|
-
{updatingStrategy === 'replace' && (
|
|
674
|
-
<p className="text-xs text-destructive mt-2">
|
|
675
|
-
Warning: this will remove existing facets and assets from products and variants.
|
|
676
|
-
</p>
|
|
677
|
-
)}
|
|
678
|
-
</div>
|
|
575
|
+
)}
|
|
679
576
|
</div>
|
|
680
577
|
|
|
681
|
-
<
|
|
682
|
-
<Button type="button"
|
|
683
|
-
|
|
578
|
+
<div className="pt-2 border-t">
|
|
579
|
+
<Button type="button" onClick={uploadFile} disabled={!selectedMainLanguage}>
|
|
580
|
+
Start import now
|
|
684
581
|
</Button>
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
>
|
|
690
|
-
{isUploading ? 'Uploading…' : 'Start import'}
|
|
691
|
-
</Button>
|
|
692
|
-
</DialogFooter>
|
|
693
|
-
</DialogContent>
|
|
694
|
-
</Dialog>
|
|
695
|
-
</>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
696
586
|
)
|
|
697
587
|
}
|
package/src/dashboard/index.tsx
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
DashboardRouteDefinition,
|
|
4
4
|
PageTitle,
|
|
5
5
|
Page,
|
|
6
|
-
|
|
6
|
+
PageLayout,
|
|
7
|
+
PageBlock,
|
|
7
8
|
} from '@vendure/dashboard'
|
|
8
|
-
import { HelpCircleIcon } from 'lucide-react'
|
|
9
9
|
import { ExportProductsBulkAction } from './bulk-export'
|
|
10
10
|
import { ProductImportBlock } from './import'
|
|
11
11
|
import { ProductExportBlock } from './export'
|
|
@@ -16,40 +16,41 @@ export const productImportRoute: DashboardRouteDefinition = {
|
|
|
16
16
|
sectionId: 'catalog',
|
|
17
17
|
id: 'product-importer',
|
|
18
18
|
url: '/product-importer',
|
|
19
|
-
title: 'Import/export',
|
|
19
|
+
title: 'Import/export products',
|
|
20
20
|
},
|
|
21
21
|
path: '/product-importer',
|
|
22
22
|
loader: () => ({
|
|
23
|
-
breadcrumb: 'Import
|
|
23
|
+
breadcrumb: 'Import/export products',
|
|
24
24
|
}),
|
|
25
25
|
component: () => (
|
|
26
26
|
<Page pageId="product-import">
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<a
|
|
35
|
-
href="https://wearehaustech.github.io/docs/vendure-plugins/product-import-export-plugin"
|
|
36
|
-
target="_blank"
|
|
37
|
-
rel="noreferrer"
|
|
38
|
-
className="hidden sm:inline-flex"
|
|
27
|
+
<PageTitle>Import/export products</PageTitle>
|
|
28
|
+
<PageLayout>
|
|
29
|
+
<PageBlock
|
|
30
|
+
column="main"
|
|
31
|
+
blockId="main-form-import"
|
|
32
|
+
title="Import products"
|
|
33
|
+
description="Import products from a CSV file"
|
|
39
34
|
>
|
|
40
|
-
<Button variant="outline" size="sm">
|
|
41
|
-
<HelpCircleIcon className="mr-2 h-4 w-4" />
|
|
42
|
-
Help & guide
|
|
43
|
-
</Button>
|
|
44
|
-
</a>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="space-y-6 w-full">
|
|
47
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full">
|
|
48
35
|
<ProductImportBlock />
|
|
36
|
+
</PageBlock>
|
|
37
|
+
<PageBlock
|
|
38
|
+
column="main"
|
|
39
|
+
blockId="main-form-export"
|
|
40
|
+
title="Export products"
|
|
41
|
+
description="Exports all products to a CSV file. The export is added to the job queue and will be processed in the background. When the file is ready, you will receive an email and the exported file will be available to download from the list below."
|
|
42
|
+
>
|
|
49
43
|
<ProductExportBlock />
|
|
50
|
-
</
|
|
51
|
-
<
|
|
52
|
-
|
|
44
|
+
</PageBlock>
|
|
45
|
+
<PageBlock
|
|
46
|
+
column="main"
|
|
47
|
+
blockId="main-form-exported-list"
|
|
48
|
+
title="Exported files"
|
|
49
|
+
description="Files available for download"
|
|
50
|
+
>
|
|
51
|
+
<ExportedList />
|
|
52
|
+
</PageBlock>
|
|
53
|
+
</PageLayout>
|
|
53
54
|
</Page>
|
|
54
55
|
),
|
|
55
56
|
}
|