@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +38 -11
  2. package/README.mdx +69 -59
  3. package/package.json +7 -9
  4. package/src/api/plugin.controller.d.ts +32 -2
  5. package/src/api/plugin.controller.js +30 -4
  6. package/src/api/plugin.controller.js.map +1 -1
  7. package/src/api/product-export.controller.d.ts +1 -0
  8. package/src/api/product-export.controller.js +17 -0
  9. package/src/api/product-export.controller.js.map +1 -1
  10. package/src/api/product-import.controller.js +1 -0
  11. package/src/api/product-import.controller.js.map +1 -1
  12. package/src/dashboard/bulk-export.tsx +0 -1
  13. package/src/dashboard/export-dialog.tsx +285 -22
  14. package/src/dashboard/export.tsx +17 -52
  15. package/src/dashboard/exported-list.tsx +96 -176
  16. package/src/dashboard/import.tsx +149 -286
  17. package/src/dashboard/index.tsx +28 -27
  18. package/src/dashboard/utils.ts +20 -3
  19. package/src/index.d.ts +1 -1
  20. package/src/product-import-export.plugin.d.ts +0 -6
  21. package/src/product-import-export.plugin.js +25 -29
  22. package/src/product-import-export.plugin.js.map +1 -1
  23. package/src/providers/import-providers/asset-importer.js +8 -27
  24. package/src/providers/import-providers/asset-importer.js.map +1 -1
  25. package/src/providers/import-providers/import-parser.d.ts +0 -1
  26. package/src/providers/import-providers/import-parser.js +2 -18
  27. package/src/providers/import-providers/import-parser.js.map +1 -1
  28. package/src/providers/import-providers/product-importer.d.ts +0 -10
  29. package/src/providers/import-providers/product-importer.js +34 -115
  30. package/src/providers/import-providers/product-importer.js.map +1 -1
  31. package/src/services/export-storage/export-storage-strategy.d.ts +0 -8
  32. package/src/services/extended-fast-importer.service.js +12 -20
  33. package/src/services/extended-fast-importer.service.js.map +1 -1
  34. package/src/services/import-storage/import-job-storage-strategy.d.ts +0 -9
  35. package/src/services/product-export-queue.service.js +1 -1
  36. package/src/services/product-export-queue.service.js.map +1 -1
  37. package/src/services/product-export.service.d.ts +13 -7
  38. package/src/services/product-export.service.js +96 -43
  39. package/src/services/product-export.service.js.map +1 -1
  40. package/src/types.d.ts +29 -9
  41. package/src/ui/components/product-import.component.ts +1 -0
  42. package/README.md +0 -231
  43. package/src/dashboard/export-configuration-panel.tsx +0 -404
  44. package/src/dashboard/use-active-channel-key.ts +0 -7
@@ -1,34 +1,9 @@
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'
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
- const TEMPLATE_URL =
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
- }, [activeChannelId])
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
- handleFile(input.files[0])
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
- const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
478
- event.preventDefault()
479
- setIsDragActive(false)
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 (!selectedFile || !selectedMainLanguage || !updatingStrategy) {
504
- toast.error('No file selected or main language not selected.')
505
- return
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
- 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 {
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
- } catch (error: unknown) {
533
- console.error('Upload error:', error)
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
- <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 &amp; 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 &nbsp;•&nbsp; 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
- {config?.visibleOptions?.includes('updateProductSlug') && (
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
- <Switch
645
- id="update-slug"
646
- checked={updateProductSlug}
647
- onCheckedChange={setUpdateProductSlug}
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
- <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.
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
- </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>
575
+ )}
701
576
  </div>
702
577
 
703
- <DialogFooter>
704
- <Button
705
- type="button"
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
- <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
- </>
582
+ </div>
583
+ </div>
584
+ )}
585
+ </div>
723
586
  )
724
587
  }