@haus-tech/product-import-export-plugin 3.6.2 → 3.6.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.
- package/CHANGELOG.md +38 -11
- package/README.mdx +69 -59
- package/package.json +7 -9
- package/src/api/plugin.controller.d.ts +32 -2
- package/src/api/plugin.controller.js +30 -4
- package/src/api/plugin.controller.js.map +1 -1
- package/src/api/product-export.controller.d.ts +1 -0
- package/src/api/product-export.controller.js +17 -0
- package/src/api/product-export.controller.js.map +1 -1
- package/src/api/product-import.controller.js +1 -0
- package/src/api/product-import.controller.js.map +1 -1
- package/src/dashboard/bulk-export.tsx +0 -1
- package/src/dashboard/export-dialog.tsx +285 -22
- package/src/dashboard/export.tsx +17 -52
- package/src/dashboard/exported-list.tsx +96 -176
- package/src/dashboard/import.tsx +149 -286
- package/src/dashboard/index.tsx +28 -27
- package/src/dashboard/utils.ts +20 -3
- package/src/index.d.ts +1 -1
- package/src/product-import-export.plugin.d.ts +0 -6
- package/src/product-import-export.plugin.js +25 -29
- package/src/product-import-export.plugin.js.map +1 -1
- 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 +0 -10
- package/src/providers/import-providers/product-importer.js +34 -115
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/export-storage/export-storage-strategy.d.ts +0 -8
- package/src/services/extended-fast-importer.service.js +12 -20
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/import-storage/import-job-storage-strategy.d.ts +0 -9
- package/src/services/product-export-queue.service.js +1 -1
- package/src/services/product-export-queue.service.js.map +1 -1
- package/src/services/product-export.service.d.ts +13 -7
- package/src/services/product-export.service.js +96 -43
- package/src/services/product-export.service.js.map +1 -1
- package/src/types.d.ts +29 -9
- package/src/ui/components/product-import.component.ts +1 -0
- package/README.md +0 -231
- package/src/dashboard/export-configuration-panel.tsx +0 -404
- package/src/dashboard/use-active-channel-key.ts +0 -7
package/src/dashboard/import.tsx
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
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
|
-
import { useActiveChannelKey } from './use-active-channel-key'
|
|
32
7
|
import {
|
|
33
8
|
getServerLocation,
|
|
34
9
|
getChannelHeader,
|
|
@@ -37,43 +12,19 @@ import {
|
|
|
37
12
|
type PluginInitOptions,
|
|
38
13
|
} from './utils'
|
|
39
14
|
|
|
40
|
-
|
|
41
|
-
'https://github.com/WeAreHausTech/haus-tech-vendure-plugins-public/blob/main/packages/product-import-export-plugin/README.mdx#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.mdx'
|
|
44
|
-
|
|
45
|
-
const MAX_SIZE_BYTES = 20 * 1024 * 1024
|
|
46
|
-
|
|
15
|
+
// Import page component
|
|
47
16
|
export function ProductImportBlock() {
|
|
48
17
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
49
18
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
50
19
|
const [validateFile, setValidateFile] = useState<File | null>(null)
|
|
51
|
-
const [isDragActive, setIsDragActive] = useState(false)
|
|
52
|
-
const [optionsOpen, setOptionsOpen] = useState(false)
|
|
53
20
|
const [updateProductSlug, setUpdateProductSlug] = useState(true)
|
|
54
21
|
const [selectedMainLanguage, setSelectedMainLanguage] = useState<LanguageCode | undefined>()
|
|
55
22
|
const [availableLanguages, setAvailableLanguages] = useState<LanguageCode[]>([])
|
|
56
23
|
const [updatingStrategy, setUpdatingStrategy] = useState<UpdatingStrategy>('merge')
|
|
57
24
|
const [config, setConfig] = useState<PluginInitOptions | null>(null)
|
|
58
25
|
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])
|
|
73
26
|
|
|
74
27
|
useEffect(() => {
|
|
75
|
-
if (!activeChannelId) return
|
|
76
|
-
|
|
77
28
|
const fetchConfig = async () => {
|
|
78
29
|
try {
|
|
79
30
|
const serverPath = getServerLocation()
|
|
@@ -109,7 +60,7 @@ export function ProductImportBlock() {
|
|
|
109
60
|
}
|
|
110
61
|
|
|
111
62
|
fetchConfig()
|
|
112
|
-
}, [
|
|
63
|
+
}, [])
|
|
113
64
|
|
|
114
65
|
const splitLines = (text: string): string[] => {
|
|
115
66
|
return text.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)
|
|
@@ -124,6 +75,7 @@ export function ProductImportBlock() {
|
|
|
124
75
|
endChar: string
|
|
125
76
|
isJson?: boolean
|
|
126
77
|
} => {
|
|
78
|
+
// Replace starting and ending double quotes if exists
|
|
127
79
|
const trimmedText = text.trim().replace(/^"/, '').replace(/"$/, '')
|
|
128
80
|
|
|
129
81
|
return {
|
|
@@ -166,6 +118,7 @@ export function ProductImportBlock() {
|
|
|
166
118
|
}
|
|
167
119
|
for (const lang of languageCodes) {
|
|
168
120
|
if (lang) {
|
|
121
|
+
// Check if lang is defined
|
|
169
122
|
const langSpecificColumn = `${baseColumn}:${lang}`
|
|
170
123
|
const langColIndex = header.indexOf(langSpecificColumn)
|
|
171
124
|
const colValue = row[langColIndex]
|
|
@@ -220,6 +173,7 @@ export function ProductImportBlock() {
|
|
|
220
173
|
|
|
221
174
|
const header = splitLines(lines[0]).map((col) => col.trim())
|
|
222
175
|
|
|
176
|
+
// Fetch channel if not already loaded (matches Angular implementation)
|
|
223
177
|
let channel = currentChannel
|
|
224
178
|
if (!channel) {
|
|
225
179
|
try {
|
|
@@ -233,6 +187,7 @@ export function ProductImportBlock() {
|
|
|
233
187
|
credentials: 'include',
|
|
234
188
|
})
|
|
235
189
|
channel = await channelRes.json()
|
|
190
|
+
// Update state for next time
|
|
236
191
|
setCurrentChannel(channel)
|
|
237
192
|
} catch (error) {
|
|
238
193
|
console.error('Failed to fetch channel:', error)
|
|
@@ -260,6 +215,7 @@ export function ProductImportBlock() {
|
|
|
260
215
|
}
|
|
261
216
|
|
|
262
217
|
if (!selectedMainLanguage && languageCodes.length === 0) {
|
|
218
|
+
// No language codes in CSV, return channel languages so user can select
|
|
263
219
|
resolve({
|
|
264
220
|
isValid: true,
|
|
265
221
|
langCodes: availableLanguages.map((lang) => lang) as (string | undefined)[],
|
|
@@ -296,6 +252,7 @@ export function ProductImportBlock() {
|
|
|
296
252
|
|
|
297
253
|
const nameColumn = validationLanguage ? `name:${validationLanguage}` : 'name'
|
|
298
254
|
|
|
255
|
+
// Get optionGroups and optionValues columns for the main language
|
|
299
256
|
const optionGroupsColumn = validationLanguage
|
|
300
257
|
? `optionGroups:${validationLanguage}`
|
|
301
258
|
: 'optionGroups'
|
|
@@ -306,13 +263,17 @@ export function ProductImportBlock() {
|
|
|
306
263
|
const optionGroupsIndex = header.indexOf(optionGroupsColumn)
|
|
307
264
|
const optionValuesIndex = header.indexOf(optionValuesColumn)
|
|
308
265
|
|
|
266
|
+
// Loop through each row to validate the option values
|
|
309
267
|
let currentOptionGroupCount = 0
|
|
310
268
|
|
|
311
269
|
for (let i = 1; i < lines.length; i++) {
|
|
312
270
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
271
|
+
|
|
272
|
+
// Check if it's a product row (it has a 'name' field)
|
|
313
273
|
const isProductRow = row[header.indexOf(nameColumn)]
|
|
314
274
|
|
|
315
275
|
if (isProductRow) {
|
|
276
|
+
// It's a new product row, get the option groups
|
|
316
277
|
const optionGroups = (row[optionGroupsIndex]?.split('|') ?? []).filter(
|
|
317
278
|
(group) => group !== '',
|
|
318
279
|
)
|
|
@@ -321,6 +282,7 @@ export function ProductImportBlock() {
|
|
|
321
282
|
)
|
|
322
283
|
currentOptionGroupCount = optionGroups.length
|
|
323
284
|
|
|
285
|
+
// Validate that the number of option values matches the number of option groups
|
|
324
286
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
325
287
|
toast.error(
|
|
326
288
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -329,10 +291,12 @@ export function ProductImportBlock() {
|
|
|
329
291
|
return
|
|
330
292
|
}
|
|
331
293
|
} else {
|
|
294
|
+
// It's a variant row, validate the option values
|
|
332
295
|
const optionValues = (row[optionValuesIndex]?.split('|') ?? []).filter(
|
|
333
296
|
(value) => value !== '',
|
|
334
297
|
)
|
|
335
298
|
|
|
299
|
+
// Validate that the number of option values matches the product's option groups
|
|
336
300
|
if (optionValues.length !== currentOptionGroupCount) {
|
|
337
301
|
toast.error(
|
|
338
302
|
`Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
|
|
@@ -343,6 +307,7 @@ export function ProductImportBlock() {
|
|
|
343
307
|
}
|
|
344
308
|
}
|
|
345
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.
|
|
346
311
|
for (let i = 1; i < lines.length; i++) {
|
|
347
312
|
const row = splitLines(lines[i]).map((col) => col.trim())
|
|
348
313
|
for (const col of header) {
|
|
@@ -373,6 +338,7 @@ export function ProductImportBlock() {
|
|
|
373
338
|
}
|
|
374
339
|
}
|
|
375
340
|
|
|
341
|
+
// Check for missing translation values in translatable base columns
|
|
376
342
|
const { errorRows: missingBaseTranslatableValuesRows } = checkTranslatabeColumns(
|
|
377
343
|
lines,
|
|
378
344
|
translatableBaseColumns,
|
|
@@ -389,6 +355,7 @@ export function ProductImportBlock() {
|
|
|
389
355
|
return
|
|
390
356
|
}
|
|
391
357
|
|
|
358
|
+
// Check if all translatable columns have values for the main language
|
|
392
359
|
const { errorRows, errorColumns } = checkTranslatabeColumns(
|
|
393
360
|
lines,
|
|
394
361
|
translatableColumns,
|
|
@@ -420,15 +387,15 @@ export function ProductImportBlock() {
|
|
|
420
387
|
const { isValid, langCodes, clearFile = true } = await validateCsvStructure(file)
|
|
421
388
|
if (isValid) {
|
|
422
389
|
setSelectedFile(file)
|
|
423
|
-
setOptionsOpen(true)
|
|
424
390
|
} else if (clearFile) {
|
|
425
391
|
clearFileHandler()
|
|
426
392
|
return
|
|
427
393
|
} else {
|
|
394
|
+
// File is valid but needs language selection
|
|
428
395
|
setSelectedFile(file)
|
|
429
|
-
setOptionsOpen(true)
|
|
430
396
|
}
|
|
431
397
|
|
|
398
|
+
// Set available languages - either from CSV or from channel
|
|
432
399
|
if (langCodes && langCodes.length > 0) {
|
|
433
400
|
const filteredLangCodes = langCodes.filter((code) => code) as LanguageCode[]
|
|
434
401
|
setAvailableLanguages(filteredLangCodes)
|
|
@@ -439,6 +406,7 @@ export function ProductImportBlock() {
|
|
|
439
406
|
currentChannel?.availableLanguageCodes &&
|
|
440
407
|
currentChannel.availableLanguageCodes.length > 0
|
|
441
408
|
) {
|
|
409
|
+
// No languages in CSV headers, use channel languages as fallback
|
|
442
410
|
setAvailableLanguages(currentChannel.availableLanguageCodes)
|
|
443
411
|
if (!selectedMainLanguage) {
|
|
444
412
|
setSelectedMainLanguage(
|
|
@@ -450,35 +418,19 @@ export function ProductImportBlock() {
|
|
|
450
418
|
}
|
|
451
419
|
}
|
|
452
420
|
|
|
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
|
-
|
|
470
421
|
const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
471
422
|
const input = event.target
|
|
472
423
|
if (input.files && input.files.length > 0) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
}
|
|
476
430
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const file = event.dataTransfer.files?.[0]
|
|
481
|
-
if (file) handleFile(file)
|
|
431
|
+
setValidateFile(file)
|
|
432
|
+
doValidation(file)
|
|
433
|
+
}
|
|
482
434
|
}
|
|
483
435
|
|
|
484
436
|
const clearFileHandler = () => {
|
|
@@ -489,7 +441,6 @@ export function ProductImportBlock() {
|
|
|
489
441
|
setValidateFile(null)
|
|
490
442
|
setSelectedMainLanguage(undefined)
|
|
491
443
|
setUpdatingStrategy('merge')
|
|
492
|
-
setOptionsOpen(false)
|
|
493
444
|
}
|
|
494
445
|
|
|
495
446
|
const onMainLanguageChange = (lang: LanguageCode) => {
|
|
@@ -500,225 +451,137 @@ export function ProductImportBlock() {
|
|
|
500
451
|
}
|
|
501
452
|
|
|
502
453
|
const uploadFile = async () => {
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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)
|
|
507
460
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
clearFileHandler()
|
|
528
|
-
toast.success('The file was uploaded and has been added to the import queue.')
|
|
529
|
-
} 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)
|
|
530
480
|
toast.error('An error occurred while uploading the file.')
|
|
531
481
|
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
toast.error('An error occurred while uploading the file.')
|
|
535
|
-
} finally {
|
|
536
|
-
setIsUploading(false)
|
|
482
|
+
} else {
|
|
483
|
+
toast.error('No file selected or main language not selected.')
|
|
537
484
|
}
|
|
538
485
|
}
|
|
539
486
|
|
|
540
487
|
return (
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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>
|
|
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
|
+
))}
|
|
616
544
|
</div>
|
|
617
|
-
|
|
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>
|
|
618
550
|
</div>
|
|
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>
|
|
639
|
-
</div>
|
|
640
|
-
)}
|
|
641
551
|
|
|
642
|
-
|
|
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>
|
|
643
564
|
<div className="flex items-center space-x-2">
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
/>
|
|
649
|
-
<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>
|
|
650
569
|
</div>
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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.
|
|
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.
|
|
672
574
|
</p>
|
|
673
|
-
|
|
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>
|
|
575
|
+
)}
|
|
701
576
|
</div>
|
|
702
577
|
|
|
703
|
-
<
|
|
704
|
-
<Button
|
|
705
|
-
|
|
706
|
-
variant="outline"
|
|
707
|
-
onClick={clearFileHandler}
|
|
708
|
-
disabled={isUploading}
|
|
709
|
-
>
|
|
710
|
-
Cancel
|
|
578
|
+
<div className="pt-2 border-t">
|
|
579
|
+
<Button type="button" onClick={uploadFile} disabled={!selectedMainLanguage}>
|
|
580
|
+
Start import now
|
|
711
581
|
</Button>
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
>
|
|
717
|
-
{isUploading ? 'Uploading…' : 'Start import'}
|
|
718
|
-
</Button>
|
|
719
|
-
</DialogFooter>
|
|
720
|
-
</DialogContent>
|
|
721
|
-
</Dialog>
|
|
722
|
-
</>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
723
586
|
)
|
|
724
587
|
}
|