@haus-tech/product-import-export-plugin 3.6.3 → 3.6.5
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 +26 -54
- package/{README.mdx → README.md} +64 -35
- package/package.json +10 -10
- 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 +42 -4
- 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 +174 -45
- 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
package/src/dashboard/import.tsx
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
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'
|
|
3
28
|
import { useState, useRef, useEffect } from 'react'
|
|
4
|
-
import { toast } from 'sonner'
|
|
5
29
|
import type { LanguageCode, Channel } from '@vendure/core'
|
|
6
30
|
import { size, startsWith, endsWith, uniq } from 'lodash-es'
|
|
31
|
+
import { useActiveChannelKey } from './use-active-channel-key'
|
|
7
32
|
import {
|
|
8
33
|
getServerLocation,
|
|
9
34
|
getChannelHeader,
|
|
@@ -12,19 +37,43 @@ import {
|
|
|
12
37
|
type PluginInitOptions,
|
|
13
38
|
} from './utils'
|
|
14
39
|
|
|
15
|
-
|
|
40
|
+
const TEMPLATE_URL =
|
|
41
|
+
'https://github.com/WeAreHausTech/haus-tech-vendure-plugins-public/blob/main/packages/product-import-export-plugin/README.md#csv-format-for-import'
|
|
42
|
+
const GUIDE_URL =
|
|
43
|
+
'https://github.com/WeAreHausTech/haus-tech-vendure-plugins-public/blob/main/packages/product-import-export-plugin/README.md'
|
|
44
|
+
|
|
45
|
+
const MAX_SIZE_BYTES = 20 * 1024 * 1024
|
|
46
|
+
|
|
16
47
|
export function ProductImportBlock() {
|
|
17
48
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
18
49
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
19
50
|
const [validateFile, setValidateFile] = useState<File | null>(null)
|
|
51
|
+
const [isDragActive, setIsDragActive] = useState(false)
|
|
52
|
+
const [optionsOpen, setOptionsOpen] = useState(false)
|
|
20
53
|
const [updateProductSlug, setUpdateProductSlug] = useState(true)
|
|
21
54
|
const [selectedMainLanguage, setSelectedMainLanguage] = useState<LanguageCode | undefined>()
|
|
22
55
|
const [availableLanguages, setAvailableLanguages] = useState<LanguageCode[]>([])
|
|
23
56
|
const [updatingStrategy, setUpdatingStrategy] = useState<UpdatingStrategy>('merge')
|
|
24
57
|
const [config, setConfig] = useState<PluginInitOptions | null>(null)
|
|
25
58
|
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null)
|
|
59
|
+
const [isUploading, setIsUploading] = useState(false)
|
|
60
|
+
const activeChannelId = useActiveChannelKey()
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setSelectedFile(null)
|
|
64
|
+
setValidateFile(null)
|
|
65
|
+
setSelectedMainLanguage(undefined)
|
|
66
|
+
setAvailableLanguages([])
|
|
67
|
+
setUpdatingStrategy('merge')
|
|
68
|
+
setOptionsOpen(false)
|
|
69
|
+
if (fileInputRef.current) {
|
|
70
|
+
fileInputRef.current.value = ''
|
|
71
|
+
}
|
|
72
|
+
}, [activeChannelId])
|
|
26
73
|
|
|
27
74
|
useEffect(() => {
|
|
75
|
+
if (!activeChannelId) return
|
|
76
|
+
|
|
28
77
|
const fetchConfig = async () => {
|
|
29
78
|
try {
|
|
30
79
|
const serverPath = getServerLocation()
|
|
@@ -60,7 +109,7 @@ export function ProductImportBlock() {
|
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
fetchConfig()
|
|
63
|
-
}, [])
|
|
112
|
+
}, [activeChannelId])
|
|
64
113
|
|
|
65
114
|
const splitLines = (text: string): string[] => {
|
|
66
115
|
return text.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)
|
|
@@ -75,7 +124,6 @@ export function ProductImportBlock() {
|
|
|
75
124
|
endChar: string
|
|
76
125
|
isJson?: boolean
|
|
77
126
|
} => {
|
|
78
|
-
// Replace starting and ending double quotes if exists
|
|
79
127
|
const trimmedText = text.trim().replace(/^"/, '').replace(/"$/, '')
|
|
80
128
|
|
|
81
129
|
return {
|
|
@@ -118,7 +166,6 @@ export function ProductImportBlock() {
|
|
|
118
166
|
}
|
|
119
167
|
for (const lang of languageCodes) {
|
|
120
168
|
if (lang) {
|
|
121
|
-
// Check if lang is defined
|
|
122
169
|
const langSpecificColumn = `${baseColumn}:${lang}`
|
|
123
170
|
const langColIndex = header.indexOf(langSpecificColumn)
|
|
124
171
|
const colValue = row[langColIndex]
|
|
@@ -173,7 +220,6 @@ export function ProductImportBlock() {
|
|
|
173
220
|
|
|
174
221
|
const header = splitLines(lines[0]).map((col) => col.trim())
|
|
175
222
|
|
|
176
|
-
// Fetch channel if not already loaded (matches Angular implementation)
|
|
177
223
|
let channel = currentChannel
|
|
178
224
|
if (!channel) {
|
|
179
225
|
try {
|
|
@@ -187,7 +233,6 @@ export function ProductImportBlock() {
|
|
|
187
233
|
credentials: 'include',
|
|
188
234
|
})
|
|
189
235
|
channel = await channelRes.json()
|
|
190
|
-
// Update state for next time
|
|
191
236
|
setCurrentChannel(channel)
|
|
192
237
|
} catch (error) {
|
|
193
238
|
console.error('Failed to fetch channel:', error)
|
|
@@ -215,7 +260,6 @@ export function ProductImportBlock() {
|
|
|
215
260
|
}
|
|
216
261
|
|
|
217
262
|
if (!selectedMainLanguage && languageCodes.length === 0) {
|
|
218
|
-
// No language codes in CSV, return channel languages so user can select
|
|
219
263
|
resolve({
|
|
220
264
|
isValid: true,
|
|
221
265
|
langCodes: availableLanguages.map((lang) => lang) as (string | undefined)[],
|
|
@@ -252,7 +296,6 @@ export function ProductImportBlock() {
|
|
|
252
296
|
|
|
253
297
|
const nameColumn = validationLanguage ? `name:${validationLanguage}` : 'name'
|
|
254
298
|
|
|
255
|
-
// Get optionGroups and optionValues columns for the main language
|
|
256
299
|
const optionGroupsColumn = validationLanguage
|
|
257
300
|
? `optionGroups:${validationLanguage}`
|
|
258
301
|
: 'optionGroups'
|
|
@@ -263,17 +306,13 @@ export function ProductImportBlock() {
|
|
|
263
306
|
const optionGroupsIndex = header.indexOf(optionGroupsColumn)
|
|
264
307
|
const optionValuesIndex = header.indexOf(optionValuesColumn)
|
|
265
308
|
|
|
266
|
-
// Loop through each row to validate the option values
|
|
267
309
|
let currentOptionGroupCount = 0
|
|
268
310
|
|
|
269
311
|
for (let i = 1; i < lines.length; i++) {
|
|
270
312
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
271
|
-
|
|
272
|
-
// Check if it's a product row (it has a 'name' field)
|
|
273
313
|
const isProductRow = row[header.indexOf(nameColumn)]
|
|
274
314
|
|
|
275
315
|
if (isProductRow) {
|
|
276
|
-
// It's a new product row, get the option groups
|
|
277
316
|
const optionGroups = (row[optionGroupsIndex]?.split('|') ?? []).filter(
|
|
278
317
|
(group) => group !== '',
|
|
279
318
|
)
|
|
@@ -282,7 +321,6 @@ export function ProductImportBlock() {
|
|
|
282
321
|
)
|
|
283
322
|
currentOptionGroupCount = optionGroups.length
|
|
284
323
|
|
|
285
|
-
// Validate that the number of option values matches the number of option groups
|
|
286
324
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
287
325
|
toast.error(
|
|
288
326
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -291,12 +329,10 @@ export function ProductImportBlock() {
|
|
|
291
329
|
return
|
|
292
330
|
}
|
|
293
331
|
} else {
|
|
294
|
-
// It's a variant row, validate the option values
|
|
295
332
|
const optionValues = (row[optionValuesIndex]?.split('|') ?? []).filter(
|
|
296
333
|
(value) => value !== '',
|
|
297
334
|
)
|
|
298
335
|
|
|
299
|
-
// Validate that the number of option values matches the product's option groups
|
|
300
336
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
301
337
|
toast.error(
|
|
302
338
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -307,7 +343,6 @@ export function ProductImportBlock() {
|
|
|
307
343
|
}
|
|
308
344
|
}
|
|
309
345
|
|
|
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.
|
|
311
346
|
for (let i = 1; i < lines.length; i++) {
|
|
312
347
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
313
348
|
for (const col of header) {
|
|
@@ -338,7 +373,6 @@ export function ProductImportBlock() {
|
|
|
338
373
|
}
|
|
339
374
|
}
|
|
340
375
|
|
|
341
|
-
// Check for missing translation values in translatable base columns
|
|
342
376
|
const { errorRows: missingBaseTranslatableValuesRows } = checkTranslatabeColumns(
|
|
343
377
|
lines,
|
|
344
378
|
translatableBaseColumns,
|
|
@@ -355,7 +389,6 @@ export function ProductImportBlock() {
|
|
|
355
389
|
return
|
|
356
390
|
}
|
|
357
391
|
|
|
358
|
-
// Check if all translatable columns have values for the main language
|
|
359
392
|
const { errorRows, errorColumns } = checkTranslatabeColumns(
|
|
360
393
|
lines,
|
|
361
394
|
translatableColumns,
|
|
@@ -387,15 +420,15 @@ export function ProductImportBlock() {
|
|
|
387
420
|
const { isValid, langCodes, clearFile = true } = await validateCsvStructure(file)
|
|
388
421
|
if (isValid) {
|
|
389
422
|
setSelectedFile(file)
|
|
423
|
+
setOptionsOpen(true)
|
|
390
424
|
} else if (clearFile) {
|
|
391
425
|
clearFileHandler()
|
|
392
426
|
return
|
|
393
427
|
} else {
|
|
394
|
-
// File is valid but needs language selection
|
|
395
428
|
setSelectedFile(file)
|
|
429
|
+
setOptionsOpen(true)
|
|
396
430
|
}
|
|
397
431
|
|
|
398
|
-
// Set available languages - either from CSV or from channel
|
|
399
432
|
if (langCodes && langCodes.length > 0) {
|
|
400
433
|
const filteredLangCodes = langCodes.filter((code) => code) as LanguageCode[]
|
|
401
434
|
setAvailableLanguages(filteredLangCodes)
|
|
@@ -406,7 +439,6 @@ export function ProductImportBlock() {
|
|
|
406
439
|
currentChannel?.availableLanguageCodes &&
|
|
407
440
|
currentChannel.availableLanguageCodes.length > 0
|
|
408
441
|
) {
|
|
409
|
-
// No languages in CSV headers, use channel languages as fallback
|
|
410
442
|
setAvailableLanguages(currentChannel.availableLanguageCodes)
|
|
411
443
|
if (!selectedMainLanguage) {
|
|
412
444
|
setSelectedMainLanguage(
|
|
@@ -418,21 +450,37 @@ export function ProductImportBlock() {
|
|
|
418
450
|
}
|
|
419
451
|
}
|
|
420
452
|
|
|
453
|
+
const handleFile = (file: File) => {
|
|
454
|
+
if (file.type !== 'text/csv' && !file.name.toLowerCase().endsWith('.csv')) {
|
|
455
|
+
toast.error('Invalid file type. Please upload a CSV file.')
|
|
456
|
+
clearFileHandler()
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (file.size > MAX_SIZE_BYTES) {
|
|
461
|
+
toast.error('File is larger than 20MB.')
|
|
462
|
+
clearFileHandler()
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
setValidateFile(file)
|
|
467
|
+
doValidation(file)
|
|
468
|
+
}
|
|
469
|
+
|
|
421
470
|
const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
422
471
|
const input = event.target
|
|
423
472
|
if (input.files && input.files.length > 0) {
|
|
424
|
-
|
|
425
|
-
if (file.type !== 'text/csv') {
|
|
426
|
-
toast.error('Invalid file type. Please upload a CSV file.')
|
|
427
|
-
clearFileHandler()
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
setValidateFile(file)
|
|
432
|
-
doValidation(file)
|
|
473
|
+
handleFile(input.files[0])
|
|
433
474
|
}
|
|
434
475
|
}
|
|
435
476
|
|
|
477
|
+
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
|
478
|
+
event.preventDefault()
|
|
479
|
+
setIsDragActive(false)
|
|
480
|
+
const file = event.dataTransfer.files?.[0]
|
|
481
|
+
if (file) handleFile(file)
|
|
482
|
+
}
|
|
483
|
+
|
|
436
484
|
const clearFileHandler = () => {
|
|
437
485
|
if (fileInputRef.current) {
|
|
438
486
|
fileInputRef.current.value = ''
|
|
@@ -441,6 +489,7 @@ export function ProductImportBlock() {
|
|
|
441
489
|
setValidateFile(null)
|
|
442
490
|
setSelectedMainLanguage(undefined)
|
|
443
491
|
setUpdatingStrategy('merge')
|
|
492
|
+
setOptionsOpen(false)
|
|
444
493
|
}
|
|
445
494
|
|
|
446
495
|
const onMainLanguageChange = (lang: LanguageCode) => {
|
|
@@ -451,137 +500,225 @@ export function ProductImportBlock() {
|
|
|
451
500
|
}
|
|
452
501
|
|
|
453
502
|
const uploadFile = async () => {
|
|
454
|
-
if (selectedFile
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
formData.append('mainLanguage', selectedMainLanguage)
|
|
459
|
-
formData.append('updatingStrategy', updatingStrategy)
|
|
503
|
+
if (!selectedFile || !selectedMainLanguage || !updatingStrategy) {
|
|
504
|
+
toast.error('No file selected or main language not selected.')
|
|
505
|
+
return
|
|
506
|
+
}
|
|
460
507
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
508
|
+
setIsUploading(true)
|
|
509
|
+
const formData = new FormData()
|
|
510
|
+
formData.append('file', selectedFile)
|
|
511
|
+
formData.append('updateProductSlug', updateProductSlug.toString())
|
|
512
|
+
formData.append('mainLanguage', selectedMainLanguage)
|
|
513
|
+
formData.append('updatingStrategy', updatingStrategy)
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const serverPath = getServerLocation()
|
|
517
|
+
const res = await fetch(`${serverPath}/product-import/upload`, {
|
|
518
|
+
method: 'POST',
|
|
519
|
+
body: formData,
|
|
520
|
+
headers: {
|
|
521
|
+
...getChannelHeader(),
|
|
522
|
+
},
|
|
523
|
+
credentials: 'include',
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
if (res.ok) {
|
|
527
|
+
clearFileHandler()
|
|
528
|
+
toast.success('The file was uploaded and has been added to the import queue.')
|
|
529
|
+
} else {
|
|
480
530
|
toast.error('An error occurred while uploading the file.')
|
|
481
531
|
}
|
|
482
|
-
}
|
|
483
|
-
|
|
532
|
+
} catch (error: unknown) {
|
|
533
|
+
console.error('Upload error:', error)
|
|
534
|
+
toast.error('An error occurred while uploading the file.')
|
|
535
|
+
} finally {
|
|
536
|
+
setIsUploading(false)
|
|
484
537
|
}
|
|
485
538
|
}
|
|
486
539
|
|
|
487
540
|
return (
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
541
|
+
<>
|
|
542
|
+
<Card className="h-full">
|
|
543
|
+
<CardHeader className="flex flex-row items-start gap-3 space-y-0 items-center">
|
|
544
|
+
<div className="bg-primary/10 text-primary rounded-md p-2 flex h-9 w-9 items-center justify-center">
|
|
545
|
+
<UploadIcon className="h-5 w-5" />
|
|
546
|
+
</div>
|
|
547
|
+
<div className="space-y-1">
|
|
548
|
+
<CardTitle className="mb-0">Import products</CardTitle>
|
|
549
|
+
<CardDescription className="text-xs">
|
|
550
|
+
Upload a CSV file to create or update products.
|
|
551
|
+
</CardDescription>
|
|
552
|
+
</div>
|
|
553
|
+
</CardHeader>
|
|
554
|
+
<CardContent className="space-y-4">
|
|
555
|
+
<div
|
|
556
|
+
onDragOver={(e) => {
|
|
557
|
+
e.preventDefault()
|
|
558
|
+
setIsDragActive(true)
|
|
559
|
+
}}
|
|
560
|
+
onDragLeave={() => setIsDragActive(false)}
|
|
561
|
+
onDrop={onDrop}
|
|
562
|
+
onClick={() => fileInputRef.current?.click()}
|
|
563
|
+
role="button"
|
|
564
|
+
tabIndex={0}
|
|
565
|
+
onKeyDown={(e) => {
|
|
566
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
567
|
+
fileInputRef.current?.click()
|
|
568
|
+
}
|
|
569
|
+
}}
|
|
570
|
+
className={`border-2 border-dashed rounded-lg px-6 py-10 text-center cursor-pointer transition-colors ${
|
|
571
|
+
isDragActive
|
|
572
|
+
? 'border-primary bg-primary/5'
|
|
573
|
+
: 'border-border hover:border-primary/40 hover:bg-muted/40'
|
|
574
|
+
}`}
|
|
575
|
+
>
|
|
576
|
+
<input
|
|
577
|
+
ref={fileInputRef}
|
|
578
|
+
type="file"
|
|
579
|
+
id="file-upload"
|
|
580
|
+
accept=".csv"
|
|
581
|
+
onChange={onFileSelected}
|
|
582
|
+
className="hidden"
|
|
583
|
+
/>
|
|
584
|
+
{selectedFile ? (
|
|
585
|
+
<div className="flex flex-col items-center gap-2">
|
|
586
|
+
<FileTextIcon className="h-8 w-8 text-primary" />
|
|
587
|
+
<p className="text-sm font-medium">{selectedFile.name}</p>
|
|
588
|
+
<p className="text-xs text-muted-foreground">
|
|
589
|
+
{(selectedFile.size / 1024).toFixed(1)} KB
|
|
590
|
+
</p>
|
|
591
|
+
<Button
|
|
592
|
+
type="button"
|
|
593
|
+
variant="ghost"
|
|
594
|
+
size="sm"
|
|
595
|
+
onClick={(e) => {
|
|
596
|
+
e.stopPropagation()
|
|
597
|
+
clearFileHandler()
|
|
598
|
+
}}
|
|
599
|
+
>
|
|
600
|
+
<XIcon className="mr-1 h-3 w-3" /> Remove
|
|
601
|
+
</Button>
|
|
602
|
+
</div>
|
|
603
|
+
) : (
|
|
604
|
+
<div className="flex flex-col items-center gap-3">
|
|
605
|
+
<UploadCloudIcon className="h-10 w-10 text-muted-foreground" />
|
|
606
|
+
<div className="space-y-1">
|
|
607
|
+
<p className="text-sm font-medium">Drag & drop your CSV file here</p>
|
|
608
|
+
<p className="text-xs text-muted-foreground">or</p>
|
|
609
|
+
</div>
|
|
610
|
+
<Button type="button" variant="outline" size="sm">
|
|
611
|
+
Choose file
|
|
612
|
+
</Button>
|
|
613
|
+
<p className="text-xs text-muted-foreground">
|
|
614
|
+
CSV UTF-8 only • Max file size: 20MB
|
|
615
|
+
</p>
|
|
544
616
|
</div>
|
|
545
|
-
|
|
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>
|
|
617
|
+
)}
|
|
550
618
|
</div>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
619
|
+
</CardContent>
|
|
620
|
+
</Card>
|
|
621
|
+
|
|
622
|
+
<Dialog
|
|
623
|
+
open={optionsOpen}
|
|
624
|
+
onOpenChange={(open) => (open ? setOptionsOpen(true) : clearFileHandler())}
|
|
625
|
+
>
|
|
626
|
+
<DialogContent className="max-w-lg">
|
|
627
|
+
<DialogHeader>
|
|
628
|
+
<DialogTitle>Import options</DialogTitle>
|
|
629
|
+
<DialogDescription>
|
|
630
|
+
Configure how this CSV should be imported into your catalog.
|
|
631
|
+
</DialogDescription>
|
|
632
|
+
</DialogHeader>
|
|
633
|
+
|
|
634
|
+
<div className="space-y-4">
|
|
635
|
+
{selectedFile && (
|
|
636
|
+
<div className="bg-muted/40 rounded-md px-3 py-2 text-sm flex items-center gap-2">
|
|
637
|
+
<FileTextIcon className="h-4 w-4 text-primary" />
|
|
638
|
+
<span className="font-medium truncate">{selectedFile.name}</span>
|
|
563
639
|
</div>
|
|
640
|
+
)}
|
|
641
|
+
|
|
642
|
+
{config?.visibleOptions?.includes('updateProductSlug') && (
|
|
564
643
|
<div className="flex items-center space-x-2">
|
|
565
|
-
<
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
644
|
+
<Switch
|
|
645
|
+
id="update-slug"
|
|
646
|
+
checked={updateProductSlug}
|
|
647
|
+
onCheckedChange={setUpdateProductSlug}
|
|
648
|
+
/>
|
|
649
|
+
<Label htmlFor="update-slug">Update product slugs based on name</Label>
|
|
569
650
|
</div>
|
|
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.
|
|
574
|
-
</p>
|
|
575
651
|
)}
|
|
652
|
+
|
|
653
|
+
<div>
|
|
654
|
+
<Label>Main language</Label>
|
|
655
|
+
<RadioGroup
|
|
656
|
+
value={selectedMainLanguage || ''}
|
|
657
|
+
onValueChange={(val) => onMainLanguageChange(val as LanguageCode)}
|
|
658
|
+
>
|
|
659
|
+
<div className="flex flex-wrap gap-3 mt-2">
|
|
660
|
+
{availableLanguages.map((lang) => (
|
|
661
|
+
<div key={lang} className="flex items-center space-x-2">
|
|
662
|
+
<RadioGroupItem value={lang} id={`lang-${lang}`} />
|
|
663
|
+
<Label htmlFor={`lang-${lang}`} className="font-normal uppercase">
|
|
664
|
+
{lang}
|
|
665
|
+
</Label>
|
|
666
|
+
</div>
|
|
667
|
+
))}
|
|
668
|
+
</div>
|
|
669
|
+
</RadioGroup>
|
|
670
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
671
|
+
Used as a fallback when a translation is missing.
|
|
672
|
+
</p>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<div>
|
|
676
|
+
<Label>Strategy for facets and assets</Label>
|
|
677
|
+
<RadioGroup
|
|
678
|
+
value={updatingStrategy}
|
|
679
|
+
onValueChange={(val) => setUpdatingStrategy(val as UpdatingStrategy)}
|
|
680
|
+
className="mt-2"
|
|
681
|
+
>
|
|
682
|
+
<div className="flex items-center space-x-2">
|
|
683
|
+
<RadioGroupItem value="merge" id="merge" />
|
|
684
|
+
<Label htmlFor="merge" className="font-normal">
|
|
685
|
+
Merge
|
|
686
|
+
</Label>
|
|
687
|
+
</div>
|
|
688
|
+
<div className="flex items-center space-x-2">
|
|
689
|
+
<RadioGroupItem value="replace" id="replace" />
|
|
690
|
+
<Label htmlFor="replace" className="font-normal">
|
|
691
|
+
Overwrite
|
|
692
|
+
</Label>
|
|
693
|
+
</div>
|
|
694
|
+
</RadioGroup>
|
|
695
|
+
{updatingStrategy === 'replace' && (
|
|
696
|
+
<p className="text-xs text-destructive mt-2">
|
|
697
|
+
Warning: this will remove existing facets and assets from products and variants.
|
|
698
|
+
</p>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
576
701
|
</div>
|
|
577
702
|
|
|
578
|
-
<
|
|
579
|
-
<Button
|
|
580
|
-
|
|
703
|
+
<DialogFooter>
|
|
704
|
+
<Button
|
|
705
|
+
type="button"
|
|
706
|
+
variant="outline"
|
|
707
|
+
onClick={clearFileHandler}
|
|
708
|
+
disabled={isUploading}
|
|
709
|
+
>
|
|
710
|
+
Cancel
|
|
581
711
|
</Button>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
712
|
+
<Button
|
|
713
|
+
type="button"
|
|
714
|
+
onClick={uploadFile}
|
|
715
|
+
disabled={!selectedMainLanguage || isUploading}
|
|
716
|
+
>
|
|
717
|
+
{isUploading ? 'Uploading…' : 'Start import'}
|
|
718
|
+
</Button>
|
|
719
|
+
</DialogFooter>
|
|
720
|
+
</DialogContent>
|
|
721
|
+
</Dialog>
|
|
722
|
+
</>
|
|
586
723
|
)
|
|
587
724
|
}
|