@haus-tech/product-import-export-plugin 3.0.27-4 → 3.0.29-0

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 CHANGED
@@ -1,3 +1,14 @@
1
+ ## 3.0.28-0
2
+
3
+ ### 🚀 Features
4
+
5
+ - **product-import-export-plugin:** replace request header logic with channel-specific headers ([dfc781c](https://github.com/WeAreHausTech/haus-tech-vendure-plugins/commit/dfc781c))
6
+ - **product-import-export-plugin:** enhance import/export functionality and dashboard integration ([825ee89](https://github.com/WeAreHausTech/haus-tech-vendure-plugins/commit/825ee89))
7
+
8
+ ## 3.0.27-4
9
+
10
+ This was a version bump only for product-import-export-plugin to align it with other projects, there were no code changes.
11
+
1
12
  ## 3.0.27-3
2
13
 
3
14
  This was a version bump only for product-import-export-plugin to align it with other projects, there were no code changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/product-import-export-plugin",
3
- "version": "3.0.27-4",
3
+ "version": "3.0.29-0",
4
4
  "description": "A Vendure plugin for importing and exporting products using a CSV file",
5
5
  "author": "Haus Tech",
6
6
  "repository": {
@@ -0,0 +1,337 @@
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ Input,
10
+ Label,
11
+ RadioGroup,
12
+ RadioGroupItem,
13
+ DataTableBulkActionItem,
14
+ } from '@vendure/dashboard'
15
+ import { DownloadIcon } from 'lucide-react'
16
+ import { useState, useEffect } from 'react'
17
+ import { toast } from 'sonner'
18
+ import {
19
+ getServerLocation,
20
+ getChannelHeader,
21
+ type PluginInitOptions,
22
+ type ExportFields,
23
+ type ProductFields,
24
+ type VariantFields,
25
+ } from './utils'
26
+
27
+ // Export Dialog Component
28
+ function ExportDialog({
29
+ open,
30
+ onOpenChange,
31
+ productIds,
32
+ }: {
33
+ open: boolean
34
+ onOpenChange: (open: boolean) => void
35
+ productIds: string[]
36
+ }) {
37
+ const [fileName, setFileName] = useState('')
38
+ const [customFields, setCustomFields] = useState<string[]>([])
39
+ const [selectedFields, setSelectedFields] = useState<string[]>([])
40
+ const [exportAssetsAs, setExportAssetsAs] = useState<'url' | 'json'>('url')
41
+ const [selectedExportFields, setSelectedExportFields] = useState<ExportFields>([])
42
+ const [availableExportFields] = useState<ExportFields>([
43
+ 'productId',
44
+ 'name',
45
+ 'slug',
46
+ 'description',
47
+ 'assets',
48
+ 'facets',
49
+ 'optionGroups',
50
+ 'sku',
51
+ 'optionValues',
52
+ 'price',
53
+ 'taxCategory',
54
+ 'stockOnHand',
55
+ 'trackInventory',
56
+ 'variantAssets',
57
+ 'variantFacets',
58
+ 'enabled',
59
+ ])
60
+ const [config, setConfig] = useState<PluginInitOptions['exportOptions'] | null>(null)
61
+ const [toggleAllChecked, setToggleAllChecked] = useState(false)
62
+
63
+ useEffect(() => {
64
+ const fetchConfig = async () => {
65
+ try {
66
+ const serverPath = getServerLocation()
67
+ const [configRes, customFieldsRes] = await Promise.all([
68
+ fetch(`${serverPath}/product-import-export/config`, {
69
+ method: 'GET',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...getChannelHeader(),
73
+ },
74
+ credentials: 'include',
75
+ }),
76
+ fetch(`${serverPath}/product-export/custom-fields`, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ ...getChannelHeader(),
81
+ },
82
+ credentials: 'include',
83
+ body: JSON.stringify(productIds),
84
+ }),
85
+ ])
86
+
87
+ const configData = await configRes.json()
88
+ const customFieldsData = await customFieldsRes.json()
89
+ setConfig(configData.exportOptions)
90
+ setCustomFields(customFieldsData.map((field: { name: string }) => field.name))
91
+ setSelectedFields([...customFieldsData.map((field: { name: string }) => field.name)])
92
+ setExportAssetsAs(
93
+ configData.exportOptions?.defaultExportAssetsAs ||
94
+ configData.exportOptions?.exportAssetsAsOptions?.[0] ||
95
+ 'url',
96
+ )
97
+ if (
98
+ configData.exportOptions?.defaultExportFields ||
99
+ configData.exportOptions?.requiredExportFields
100
+ ) {
101
+ setSelectedExportFields([
102
+ ...(configData.exportOptions.defaultExportFields || []),
103
+ ...(configData.exportOptions.requiredExportFields || []),
104
+ ])
105
+ }
106
+ } catch (error) {
107
+ console.error('Failed to fetch config:', error)
108
+ }
109
+ }
110
+
111
+ if (open && productIds.length > 0) {
112
+ fetchConfig()
113
+ }
114
+ }, [open, productIds])
115
+
116
+ const toggleFieldSelection = (fieldName: string) => {
117
+ setSelectedFields((prev) =>
118
+ prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
119
+ )
120
+ }
121
+
122
+ const toggleExportFieldSelection = (fieldName: ProductFields | VariantFields) => {
123
+ setSelectedExportFields((prev) =>
124
+ prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
125
+ )
126
+ }
127
+
128
+ const toggleSelectAll = (checked: boolean) => {
129
+ setToggleAllChecked(checked)
130
+ if (checked) {
131
+ setSelectedExportFields([...availableExportFields])
132
+ setSelectedFields([...customFields])
133
+ } else {
134
+ setSelectedExportFields([...(config?.requiredExportFields || [])])
135
+ setSelectedFields([])
136
+ }
137
+ }
138
+
139
+ const handleExport = async () => {
140
+ const trimmedFileName = fileName.trim()
141
+ const finalFileName = trimmedFileName || config?.defaultFileName || 'products_export.csv'
142
+
143
+ toast.info(`Exporting ${productIds.length} products to ${finalFileName}`)
144
+
145
+ try {
146
+ const serverPath = getServerLocation()
147
+ const res = await fetch(
148
+ `${serverPath}/product-export/export?fileName=${encodeURIComponent(finalFileName)}&customFields=${encodeURIComponent(selectedFields.join(','))}&exportAssetsAs=${exportAssetsAs}&selectedExportFields=${encodeURIComponent(selectedExportFields.join(','))}`,
149
+ {
150
+ method: 'POST',
151
+ headers: {
152
+ 'Content-Type': 'application/json',
153
+ ...getChannelHeader(),
154
+ },
155
+ credentials: 'include',
156
+ body: JSON.stringify(productIds),
157
+ },
158
+ )
159
+
160
+ if (!res.ok) {
161
+ const json = await res.json()
162
+ throw new Error(json?.message || 'Export failed')
163
+ }
164
+
165
+ console.log('res', res)
166
+ const header = res.headers.get('Content-Disposition')
167
+
168
+ const parts = header!.split(';')
169
+ const filename = parts[1].split('=')[1]
170
+ const blob = await res.blob()
171
+
172
+ const blobUrl = window.URL.createObjectURL(blob)
173
+ const a = document.createElement('a')
174
+ document.body.appendChild(a)
175
+ a.setAttribute('hidden', 'true')
176
+ a.href = blobUrl
177
+ a.download = filename
178
+ a.setAttribute('target', '_blank')
179
+ a.click()
180
+
181
+ onOpenChange(false)
182
+ toast.success('Products exported successfully')
183
+ } catch (error: any) {
184
+ console.error(error)
185
+ toast.error(error.message || 'Failed to export products')
186
+ }
187
+ }
188
+
189
+ return (
190
+ <Dialog open={open} onOpenChange={onOpenChange}>
191
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
192
+ <DialogHeader>
193
+ <DialogTitle>Export products to CSV</DialogTitle>
194
+ <DialogDescription>Configure export options</DialogDescription>
195
+ </DialogHeader>
196
+
197
+ <div className="space-y-4">
198
+ <div>
199
+ <Label htmlFor="file-name">File name (optional)</Label>
200
+ <Input
201
+ id="file-name"
202
+ value={fileName}
203
+ onChange={(e) => setFileName(e.target.value)}
204
+ placeholder={config?.defaultFileName || 'products_export.csv'}
205
+ className="mt-1"
206
+ />
207
+ </div>
208
+
209
+ <div className="flex items-center space-x-2">
210
+ <input
211
+ type="checkbox"
212
+ id="select-all"
213
+ checked={toggleAllChecked}
214
+ onChange={(e) => toggleSelectAll(e.target.checked)}
215
+ />
216
+ <Label htmlFor="select-all" className="font-normal">
217
+ Select all
218
+ </Label>
219
+ </div>
220
+
221
+ <div>
222
+ <Label>Fields to export</Label>
223
+ <div className="grid grid-cols-2 gap-2 mt-2">
224
+ {availableExportFields.map((field) => (
225
+ <div key={field} className="flex items-center space-x-2">
226
+ <input
227
+ type="checkbox"
228
+ id={`field-${field}`}
229
+ checked={selectedExportFields.includes(field)}
230
+ onChange={() => toggleExportFieldSelection(field)}
231
+ disabled={config?.requiredExportFields?.includes(field)}
232
+ />
233
+ <Label htmlFor={`field-${field}`} className="font-normal">
234
+ {field}
235
+ </Label>
236
+ </div>
237
+ ))}
238
+ </div>
239
+ </div>
240
+
241
+ {customFields.length > 0 && (
242
+ <div>
243
+ <Label>Custom fields to include in the export</Label>
244
+ <div className="grid grid-cols-2 gap-2 mt-2">
245
+ {customFields.map((field) => (
246
+ <div key={field} className="flex items-center space-x-2">
247
+ <input
248
+ type="checkbox"
249
+ id={`custom-${field}`}
250
+ checked={selectedFields.includes(field)}
251
+ onChange={() => toggleFieldSelection(field)}
252
+ />
253
+ <Label htmlFor={`custom-${field}`} className="font-normal">
254
+ {field}
255
+ </Label>
256
+ </div>
257
+ ))}
258
+ </div>
259
+ </div>
260
+ )}
261
+
262
+ <div>
263
+ <Label>Export assets as:</Label>
264
+ <RadioGroup
265
+ value={exportAssetsAs}
266
+ onValueChange={(val) => setExportAssetsAs(val as 'url' | 'json')}
267
+ >
268
+ {config?.exportAssetsAsOptions?.includes('url') && (
269
+ <div className="flex items-center space-x-2">
270
+ <RadioGroupItem value="url" id="assets-url" />
271
+ <Label htmlFor="assets-url" className="font-normal">
272
+ URL
273
+ </Label>
274
+ </div>
275
+ )}
276
+ {config?.exportAssetsAsOptions?.includes('json') && (
277
+ <div className="flex items-center space-x-2">
278
+ <RadioGroupItem value="json" id="assets-json" />
279
+ <Label htmlFor="assets-json" className="font-normal">
280
+ JSON
281
+ </Label>
282
+ </div>
283
+ )}
284
+ </RadioGroup>
285
+ </div>
286
+ </div>
287
+
288
+ <DialogFooter>
289
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
290
+ Cancel
291
+ </Button>
292
+ <Button type="button" onClick={handleExport}>
293
+ Export
294
+ </Button>
295
+ </DialogFooter>
296
+ </DialogContent>
297
+ </Dialog>
298
+ )
299
+ }
300
+
301
+ // Export Bulk Action Component for Product List
302
+ export function ExportProductsBulkAction({
303
+ selection,
304
+ table,
305
+ }: {
306
+ selection: Array<{ id: string }>
307
+ table: any
308
+ }) {
309
+ const [dialogOpen, setDialogOpen] = useState(false)
310
+ const productIds = selection.map((product) => product.id)
311
+
312
+ const handleClose = () => {
313
+ setDialogOpen(false)
314
+ table.resetRowSelection()
315
+ }
316
+
317
+ return (
318
+ <>
319
+ <DataTableBulkActionItem
320
+ onClick={() => setDialogOpen(true)}
321
+ label="Export products to CSV"
322
+ icon={DownloadIcon}
323
+ />
324
+ <ExportDialog
325
+ open={dialogOpen}
326
+ onOpenChange={(open) => {
327
+ if (open) {
328
+ setDialogOpen(true)
329
+ } else {
330
+ handleClose()
331
+ }
332
+ }}
333
+ productIds={productIds}
334
+ />
335
+ </>
336
+ )
337
+ }
@@ -0,0 +1,634 @@
1
+ import {
2
+ Button,
3
+ DashboardRouteDefinition,
4
+ Page,
5
+ PageTitle,
6
+ PageLayout,
7
+ PageBlock,
8
+ Switch,
9
+ Label,
10
+ RadioGroup,
11
+ RadioGroupItem,
12
+ } from '@vendure/dashboard'
13
+ import { UploadIcon } from 'lucide-react'
14
+ import { useState, useRef, useEffect } from 'react'
15
+ import { toast } from 'sonner'
16
+ import type { LanguageCode, Channel } from '@vendure/core'
17
+ import { size, startsWith, endsWith, uniq } from 'lodash-es'
18
+ import {
19
+ getServerLocation,
20
+ getChannelHeader,
21
+ type UpdatingStrategy,
22
+ type ValidateReturnType,
23
+ type PluginInitOptions,
24
+ } from './utils'
25
+
26
+ // Import page component
27
+ function ProductImportPage() {
28
+ const fileInputRef = useRef<HTMLInputElement>(null)
29
+ const [selectedFile, setSelectedFile] = useState<File | null>(null)
30
+ const [validateFile, setValidateFile] = useState<File | null>(null)
31
+ const [updateProductSlug, setUpdateProductSlug] = useState(true)
32
+ const [selectedMainLanguage, setSelectedMainLanguage] = useState<LanguageCode | undefined>()
33
+ const [availableLanguages, setAvailableLanguages] = useState<LanguageCode[]>([])
34
+ const [updatingStrategy, setUpdatingStrategy] = useState<UpdatingStrategy>('merge')
35
+ const [config, setConfig] = useState<PluginInitOptions | null>(null)
36
+ const [currentChannel, setCurrentChannel] = useState<Channel | null>(null)
37
+
38
+ useEffect(() => {
39
+ const fetchConfig = async () => {
40
+ try {
41
+ const serverPath = getServerLocation()
42
+ const [configRes, channelRes] = await Promise.all([
43
+ fetch(`${serverPath}/product-import-export/config`, {
44
+ method: 'GET',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...getChannelHeader(),
48
+ },
49
+ credentials: 'include',
50
+ }),
51
+ fetch(`${serverPath}/product-import-export/channel`, {
52
+ method: 'GET',
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ ...getChannelHeader(),
56
+ },
57
+ credentials: 'include',
58
+ }),
59
+ ])
60
+
61
+ const configData = await configRes.json()
62
+ const channelData = await channelRes.json()
63
+ setConfig(configData.importOptions)
64
+ setCurrentChannel(channelData)
65
+ if (configData.importOptions?.defaultOptions?.updateProductSlug !== undefined) {
66
+ setUpdateProductSlug(configData.importOptions.defaultOptions.updateProductSlug)
67
+ }
68
+ } catch (error) {
69
+ console.error('Failed to fetch config:', error)
70
+ }
71
+ }
72
+
73
+ fetchConfig()
74
+ }, [])
75
+
76
+ const splitLines = (text: string): string[] => {
77
+ return text.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)
78
+ }
79
+
80
+ const checkifJson = (
81
+ text: string,
82
+ ): {
83
+ startsWithJson: boolean
84
+ endsWithJson: boolean
85
+ startChar: string
86
+ endChar: string
87
+ isJson?: boolean
88
+ } => {
89
+ // Replace starting and ending double quotes if exists
90
+ const trimmedText = text.trim().replace(/^"/, '').replace(/"$/, '')
91
+
92
+ return {
93
+ startsWithJson: startsWith(trimmedText, '{') || startsWith(trimmedText, '['),
94
+ endsWithJson: endsWith(trimmedText, '}') || endsWith(trimmedText, ']'),
95
+ startChar: trimmedText.charAt(0),
96
+ endChar: trimmedText.charAt(trimmedText.length - 1),
97
+ isJson:
98
+ startsWith(trimmedText, '{') ||
99
+ startsWith(trimmedText, '[') ||
100
+ endsWith(trimmedText, '}') ||
101
+ endsWith(trimmedText, ']'),
102
+ }
103
+ }
104
+
105
+ const checkTranslatabeColumns = (
106
+ lines: string[],
107
+ columns: string[],
108
+ header: string[],
109
+ languageCodes: (string | undefined)[],
110
+ mainLanguage?: LanguageCode,
111
+ ): { errorRows: number[]; errorColumns: string[] } => {
112
+ const errorRows: number[] = []
113
+ const errorColumns: string[] = []
114
+
115
+ if (size(languageCodes) < 2 || !mainLanguage) {
116
+ return { errorRows, errorColumns }
117
+ }
118
+
119
+ for (let i = 1; i < lines.length; i++) {
120
+ const row = splitLines(lines[i]).map((col) => col.trim())
121
+ for (const baseColumn of columns) {
122
+ const mainLanguageColumn = `${baseColumn}:${mainLanguage}`
123
+ const colIndex = header.indexOf(mainLanguageColumn)
124
+ const mainLanguageValue = row[colIndex]
125
+ const mainLanguageHasValue = mainLanguageValue !== undefined && mainLanguageValue !== ''
126
+
127
+ if (mainLanguageHasValue) {
128
+ continue
129
+ }
130
+ for (const lang of languageCodes) {
131
+ if (lang) {
132
+ // Check if lang is defined
133
+ const langSpecificColumn = `${baseColumn}:${lang}`
134
+ const langColIndex = header.indexOf(langSpecificColumn)
135
+ const colValue = row[langColIndex]
136
+ const colHasValue = colValue !== undefined && colValue !== ''
137
+
138
+ if (!mainLanguageHasValue && colHasValue) {
139
+ errorRows.push(i + 1)
140
+ errorColumns.push(langSpecificColumn)
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return { errorRows, errorColumns }
148
+ }
149
+
150
+ const validateCsvStructure = async (file: File): Promise<ValidateReturnType> => {
151
+ const baseColumns = ['sku']
152
+ const optionalColumns = [
153
+ 'assets',
154
+ 'price',
155
+ 'taxCategory',
156
+ 'stockOnHand',
157
+ 'trackInventory',
158
+ 'variantAssets',
159
+ ]
160
+ const translatableBaseColumns = ['name']
161
+ const translatableColumns = [
162
+ 'slug',
163
+ 'description',
164
+ 'facets',
165
+ 'optionGroups',
166
+ 'optionValues',
167
+ 'variantFacets',
168
+ ]
169
+
170
+ return new Promise<ValidateReturnType>((resolve) => {
171
+ const reader = new FileReader()
172
+ reader.onload = async (e) => {
173
+ const text = e.target?.result as string
174
+ const lines = text.split('\n').filter((line) => line.trim() !== '')
175
+
176
+ if (lines.length === 0) {
177
+ toast.error('The file is empty.')
178
+ resolve({ isValid: false })
179
+ return
180
+ }
181
+
182
+ const headerLength = splitLines(lines[0]).length
183
+ const inconsistentLines = lines.filter((line) => splitLines(line).length !== headerLength)
184
+
185
+ if (inconsistentLines.length > 0) {
186
+ toast.error(
187
+ 'The columns in the file are inconsistent. Please ensure all rows have the same number of columns.',
188
+ )
189
+ resolve({ isValid: false })
190
+ return
191
+ }
192
+
193
+ const header = splitLines(lines[0]).map((col) => col.trim())
194
+
195
+ // Fetch channel if not already loaded (matches Angular implementation)
196
+ let channel = currentChannel
197
+ if (!channel) {
198
+ try {
199
+ const serverPath = getServerLocation()
200
+ const channelRes = await fetch(`${serverPath}/product-import-export/channel`, {
201
+ method: 'GET',
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ ...getChannelHeader(),
205
+ },
206
+ credentials: 'include',
207
+ })
208
+ channel = await channelRes.json()
209
+ // Update state for next time
210
+ setCurrentChannel(channel)
211
+ } catch (error) {
212
+ console.error('Failed to fetch channel:', error)
213
+ toast.error('Failed to fetch channel information.')
214
+ resolve({ isValid: false })
215
+ return
216
+ }
217
+ }
218
+
219
+ const availableLanguages = channel?.availableLanguageCodes || []
220
+
221
+ const languageCodes = header
222
+ .map((col) => col.match(/:(\w{2})$/)?.[1])
223
+ .filter((code, index, self) => code && self.indexOf(code) === index)
224
+
225
+ let validationLanguage = languageCodes.length === 0 ? undefined : selectedMainLanguage
226
+
227
+ if (!selectedMainLanguage && languageCodes.length < 2) {
228
+ validationLanguage = (languageCodes as LanguageCode[])?.[0]
229
+ }
230
+
231
+ if (!selectedMainLanguage && languageCodes.length > 1) {
232
+ resolve({ isValid: true, langCodes: languageCodes, clearFile: false })
233
+ return
234
+ }
235
+
236
+ if (!selectedMainLanguage && languageCodes.length === 0) {
237
+ // No language codes in CSV, return channel languages so user can select
238
+ resolve({
239
+ isValid: true,
240
+ langCodes: availableLanguages.map((lang) => lang) as (string | undefined)[],
241
+ clearFile: false,
242
+ })
243
+ return
244
+ }
245
+
246
+ const missingLanguages = (languageCodes as LanguageCode[]).filter(
247
+ (code) => !availableLanguages.includes(code),
248
+ )
249
+ if (missingLanguages.length > 0) {
250
+ toast.error(
251
+ `Your CSV file contains languages that are not available in this channel. Please add the languages to the channel or remove the languages that are not used and try again. Missing: ${missingLanguages.join(', ')}`,
252
+ )
253
+ resolve({ isValid: false })
254
+ return
255
+ }
256
+
257
+ const missingBaseColumns = baseColumns.filter((col) => !header.includes(col))
258
+ const missingTranslatableBaseColumns = translatableBaseColumns.filter(
259
+ (col) => !header.includes(validationLanguage ? `${col}:${validationLanguage}` : col),
260
+ )
261
+
262
+ const combinedMissingBaseColumns = [
263
+ ...missingBaseColumns,
264
+ ...missingTranslatableBaseColumns,
265
+ ]
266
+ if (combinedMissingBaseColumns.length > 0) {
267
+ toast.error(`Some columns are missing: ${combinedMissingBaseColumns.join(', ')}`)
268
+ resolve({ isValid: false })
269
+ return
270
+ }
271
+
272
+ const nameColumn = validationLanguage ? `name:${validationLanguage}` : 'name'
273
+
274
+ // Get optionGroups and optionValues columns for the main language
275
+ const optionGroupsColumn = validationLanguage
276
+ ? `optionGroups:${validationLanguage}`
277
+ : 'optionGroups'
278
+ const optionValuesColumn = validationLanguage
279
+ ? `optionValues:${validationLanguage}`
280
+ : 'optionValues'
281
+
282
+ const optionGroupsIndex = header.indexOf(optionGroupsColumn)
283
+ const optionValuesIndex = header.indexOf(optionValuesColumn)
284
+
285
+ // Loop through each row to validate the option values
286
+ let currentOptionGroupCount = 0
287
+
288
+ for (let i = 1; i < lines.length; i++) {
289
+ const row = splitLines(lines[i]).map((col) => col.trim())
290
+
291
+ // Check if it's a product row (it has a 'name' field)
292
+ const isProductRow = row[header.indexOf(nameColumn)]
293
+
294
+ if (isProductRow) {
295
+ // It's a new product row, get the option groups
296
+ const optionGroups = (row[optionGroupsIndex]?.split('|') ?? []).filter(
297
+ (group) => group !== '',
298
+ )
299
+ const optionValues = (row[optionValuesIndex]?.split('|') ?? []).filter(
300
+ (value) => value !== '',
301
+ )
302
+ currentOptionGroupCount = optionGroups.length
303
+
304
+ // Validate that the number of option values matches the number of option groups
305
+ if (optionValues.length !== currentOptionGroupCount) {
306
+ toast.error(
307
+ `Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
308
+ )
309
+ resolve({ isValid: false })
310
+ return
311
+ }
312
+ } else {
313
+ // It's a variant row, validate the option values
314
+ const optionValues = (row[optionValuesIndex]?.split('|') ?? []).filter(
315
+ (value) => value !== '',
316
+ )
317
+
318
+ // Validate that the number of option values matches the product's option groups
319
+ if (optionValues.length !== currentOptionGroupCount) {
320
+ toast.error(
321
+ `Some product variants are missing product options. Please fill in all options: Row: ${i + 1}. Expected ${currentOptionGroupCount} option values but got ${optionValues.length}.`,
322
+ )
323
+ resolve({ isValid: false })
324
+ return
325
+ }
326
+ }
327
+ }
328
+
329
+ // 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
+ for (let i = 1; i < lines.length; i++) {
331
+ const row = splitLines(lines[i]).map((col) => col.trim())
332
+ for (const col of header) {
333
+ const colIndex = header.indexOf(col)
334
+ if (colIndex !== -1 && row[colIndex]) {
335
+ try {
336
+ const { isJson } = checkifJson(row[colIndex])
337
+ if (isJson) {
338
+ JSON.parse(
339
+ row[colIndex]
340
+ .trim()
341
+ .replace(/^"/, '')
342
+ .replace(/"$/, '')
343
+ .replace(/'/g, '"')
344
+ .replace(/\s+/g, ' ')
345
+ .replace(/,\s*'/g, ", '"),
346
+ )
347
+ }
348
+ } catch (error) {
349
+ toast.error(
350
+ `Invalid JSON structure. Please upload a correct JSON file. Column: ${col}, Row: ${i + 1}`,
351
+ )
352
+ resolve({ isValid: false })
353
+ return
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ // Check for missing translation values in translatable base columns
360
+ const { errorRows: missingBaseTranslatableValuesRows } = checkTranslatabeColumns(
361
+ lines,
362
+ translatableBaseColumns,
363
+ header,
364
+ languageCodes,
365
+ selectedMainLanguage,
366
+ )
367
+
368
+ if (missingBaseTranslatableValuesRows.length > 0) {
369
+ toast.error(
370
+ `Main language is missing for name. Please add the main language and try again. Rows: ${uniq(missingBaseTranslatableValuesRows).join(', ')}`,
371
+ )
372
+ resolve({ isValid: false })
373
+ return
374
+ }
375
+
376
+ // Check if all translatable columns have values for the main language
377
+ const { errorRows, errorColumns } = checkTranslatabeColumns(
378
+ lines,
379
+ translatableColumns,
380
+ header,
381
+ languageCodes,
382
+ selectedMainLanguage,
383
+ )
384
+
385
+ if (errorRows.length > 0) {
386
+ toast.warning(
387
+ `Some fields are missing translations for the main language that exist in other languages. These will not be imported. Rows: ${uniq(errorRows).join(', ')} Columns: ${uniq(errorColumns).join(', ')}`,
388
+ )
389
+ }
390
+
391
+ resolve({ isValid: true, langCodes: languageCodes })
392
+ }
393
+
394
+ reader.onerror = (error) => {
395
+ console.error('FileReader error:', error)
396
+ toast.error('An error occurred while reading the file.')
397
+ resolve({ isValid: false })
398
+ }
399
+
400
+ reader.readAsText(file)
401
+ })
402
+ }
403
+
404
+ const doValidation = async (file: File) => {
405
+ const { isValid, langCodes, clearFile = true } = await validateCsvStructure(file)
406
+ if (isValid) {
407
+ setSelectedFile(file)
408
+ } else if (clearFile) {
409
+ clearFileHandler()
410
+ return
411
+ } else {
412
+ // File is valid but needs language selection
413
+ setSelectedFile(file)
414
+ }
415
+
416
+ // Set available languages - either from CSV or from channel
417
+ if (langCodes && langCodes.length > 0) {
418
+ const filteredLangCodes = langCodes.filter((code) => code) as LanguageCode[]
419
+ setAvailableLanguages(filteredLangCodes)
420
+ if (!selectedMainLanguage) {
421
+ setSelectedMainLanguage(filteredLangCodes.length === 1 ? filteredLangCodes[0] : undefined)
422
+ }
423
+ } else if (
424
+ currentChannel?.availableLanguageCodes &&
425
+ currentChannel.availableLanguageCodes.length > 0
426
+ ) {
427
+ // No languages in CSV headers, use channel languages as fallback
428
+ setAvailableLanguages(currentChannel.availableLanguageCodes)
429
+ if (!selectedMainLanguage) {
430
+ setSelectedMainLanguage(
431
+ currentChannel.availableLanguageCodes.length === 1
432
+ ? currentChannel.availableLanguageCodes[0]
433
+ : undefined,
434
+ )
435
+ }
436
+ }
437
+ }
438
+
439
+ const onFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
440
+ const input = event.target
441
+ if (input.files && input.files.length > 0) {
442
+ const file = input.files[0]
443
+ if (file.type !== 'text/csv') {
444
+ toast.error('Invalid file type. Please upload a CSV file.')
445
+ clearFileHandler()
446
+ return
447
+ }
448
+
449
+ setValidateFile(file)
450
+ doValidation(file)
451
+ }
452
+ }
453
+
454
+ const clearFileHandler = () => {
455
+ if (fileInputRef.current) {
456
+ fileInputRef.current.value = ''
457
+ }
458
+ setSelectedFile(null)
459
+ setValidateFile(null)
460
+ setSelectedMainLanguage(undefined)
461
+ setUpdatingStrategy('merge')
462
+ }
463
+
464
+ const onMainLanguageChange = (lang: LanguageCode) => {
465
+ setSelectedMainLanguage(lang)
466
+ if (validateFile) {
467
+ doValidation(validateFile)
468
+ }
469
+ }
470
+
471
+ const uploadFile = async () => {
472
+ if (selectedFile && selectedMainLanguage && updatingStrategy) {
473
+ const formData = new FormData()
474
+ formData.append('file', selectedFile)
475
+ formData.append('updateProductSlug', updateProductSlug.toString())
476
+ formData.append('mainLanguage', selectedMainLanguage)
477
+ formData.append('updatingStrategy', updatingStrategy)
478
+
479
+ try {
480
+ const serverPath = getServerLocation()
481
+ const res = await fetch(`${serverPath}/product-import/upload`, {
482
+ method: 'POST',
483
+ body: formData,
484
+ headers: {
485
+ ...getChannelHeader(),
486
+ },
487
+ credentials: 'include',
488
+ })
489
+
490
+ if (res.ok) {
491
+ clearFileHandler()
492
+ toast.success('The file was uploaded and has been added to the import queue.')
493
+ } else {
494
+ toast.error('An error occurred while uploading the file.')
495
+ }
496
+ } catch (error) {
497
+ toast.error('An error occurred while uploading the file.')
498
+ }
499
+ } else {
500
+ toast.error('No file selected or main language not selected.')
501
+ }
502
+ }
503
+
504
+ return (
505
+ <Page pageId="product-import">
506
+ <PageTitle>Import Products</PageTitle>
507
+ <PageLayout>
508
+ <PageBlock column="main" blockId="main-form" title="Import products from CSV">
509
+ <div className="space-y-6">
510
+ <div>
511
+ <Label htmlFor="file-upload">Choose a CSV file to import products from</Label>
512
+ <div className="flex items-center gap-2 mt-2">
513
+ <input
514
+ ref={fileInputRef}
515
+ type="file"
516
+ id="file-upload"
517
+ accept=".csv"
518
+ onChange={onFileSelected}
519
+ className="hidden"
520
+ />
521
+ {selectedFile ? (
522
+ <>
523
+ <Button
524
+ type="button"
525
+ variant="outline"
526
+ onClick={() => fileInputRef.current?.click()}
527
+ >
528
+ <UploadIcon className="mr-2 h-4 w-4" />
529
+ {selectedFile.name}
530
+ </Button>
531
+ <Button type="button" onClick={uploadFile} disabled={!selectedMainLanguage}>
532
+ Upload file
533
+ </Button>
534
+ </>
535
+ ) : (
536
+ <Button
537
+ type="button"
538
+ variant="default"
539
+ onClick={() => fileInputRef.current?.click()}
540
+ >
541
+ <UploadIcon className="mr-2 h-4 w-4" />
542
+ Select file
543
+ </Button>
544
+ )}
545
+ </div>
546
+ </div>
547
+
548
+ {selectedFile && (
549
+ <div className="space-y-4 pt-4 border-t">
550
+ {config?.visibleOptions?.includes('updateProductSlug') && (
551
+ <div className="flex items-center space-x-2">
552
+ <Switch
553
+ id="update-slug"
554
+ checked={updateProductSlug}
555
+ onCheckedChange={setUpdateProductSlug}
556
+ />
557
+ <Label htmlFor="update-slug">Update product slugs based on name</Label>
558
+ </div>
559
+ )}
560
+
561
+ <div>
562
+ <Label>Select main language:</Label>
563
+ <div className="flex flex-col gap-2 mt-2">
564
+ {availableLanguages.map((lang) => (
565
+ <div key={lang} className="flex items-center space-x-2">
566
+ <input
567
+ type="radio"
568
+ id={`lang-${lang}`}
569
+ name="selectedMainLanguage"
570
+ value={lang}
571
+ checked={selectedMainLanguage === lang}
572
+ onChange={() => onMainLanguageChange(lang)}
573
+ />
574
+ <Label htmlFor={`lang-${lang}`} className="font-normal">
575
+ {lang}
576
+ </Label>
577
+ </div>
578
+ ))}
579
+ </div>
580
+ <p className="text-sm text-muted-foreground mt-2 italic">
581
+ The main language is used as a fallback when translations are missing. It is
582
+ recommended to select the language that is used the most in your system.
583
+ </p>
584
+ </div>
585
+
586
+ <div>
587
+ <Label>Updating strategy for facets and assets:</Label>
588
+ <RadioGroup
589
+ value={updatingStrategy}
590
+ onValueChange={(val) => setUpdatingStrategy(val as UpdatingStrategy)}
591
+ >
592
+ <div className="flex items-center space-x-2">
593
+ <RadioGroupItem value="merge" id="merge" />
594
+ <Label htmlFor="merge" className="font-normal">
595
+ Merge
596
+ </Label>
597
+ </div>
598
+ <div className="flex items-center space-x-2">
599
+ <RadioGroupItem value="replace" id="replace" />
600
+ <Label htmlFor="replace" className="font-normal">
601
+ Overwrite
602
+ </Label>
603
+ </div>
604
+ </RadioGroup>
605
+ {updatingStrategy === 'replace' && (
606
+ <p className="text-sm text-red-600 mt-2">
607
+ Warning: This will remove existing facets and assets from products and
608
+ variants.
609
+ </p>
610
+ )}
611
+ </div>
612
+ </div>
613
+ )}
614
+ </div>
615
+ </PageBlock>
616
+ </PageLayout>
617
+ </Page>
618
+ )
619
+ }
620
+
621
+ // Import route definition
622
+ export const productImportRoute: DashboardRouteDefinition = {
623
+ navMenuItem: {
624
+ sectionId: 'catalog',
625
+ id: 'product-importer',
626
+ url: '/product-importer',
627
+ title: 'Import Products',
628
+ },
629
+ path: '/product-importer',
630
+ loader: () => ({
631
+ breadcrumb: 'Import Products',
632
+ }),
633
+ component: () => <ProductImportPage />,
634
+ }
@@ -0,0 +1,17 @@
1
+ import { defineDashboardExtension } from '@vendure/dashboard'
2
+ import { productImportRoute } from './import'
3
+ import { ExportProductsBulkAction } from './export'
4
+
5
+ defineDashboardExtension({
6
+ routes: [productImportRoute],
7
+ dataTables: [
8
+ {
9
+ pageId: 'product-list',
10
+ bulkActions: [
11
+ {
12
+ component: ExportProductsBulkAction,
13
+ },
14
+ ],
15
+ },
16
+ ],
17
+ })
@@ -0,0 +1,41 @@
1
+ {
2
+ "product-import.import-products": "Import products",
3
+ "product-import.card.title": "Import products from CSV",
4
+ "product-import.card.description": "Choose a CSV file to import products from",
5
+ "product-import.card.button-select": "Select file",
6
+ "product-import.card.button-upload": "Upload file",
7
+ "product-import.card.update-product-slugs": "Update product slugs based on name",
8
+ "product-import.card.select-main-language": "Select main language:",
9
+ "product-import.card.updating-strategy": "Updating strategy for facets and assets:",
10
+ "product-import.card.overwrite": "Overwrite",
11
+ "product-import.card.overwrite-warning": "Warning: This will remove existing facets and assets from products and variants.",
12
+ "product-import.card.merge": "Merge",
13
+ "product-import.card.main-language-info": "The main language is used as a fallback when translations are missing. It is recommended to select the language that is used the most in your system. The main language will be used to create products, the other languages will be used to fill in translations.",
14
+ "product-import.notifications.invalid-file-type": "Invalid file type. Please upload a CSV file.",
15
+ "product-import.notifications.invalid-file-structure": "Invalid file structure. Please upload a correct CSV file.",
16
+ "product-import.notifications.upload-success": "The file was uploaded and has been added to the import queue.",
17
+ "product-import.notifications.upload-error": "An error occurred while uploading the file.",
18
+ "product-import.notifications.no-file-selected": "No file selected.",
19
+ "product-import.notifications.mismatched-values": "Some fields are missing language specifications. Please fill in all languages:",
20
+ "product-import.notifications.missing-base-columns": "Some columns are missing:",
21
+ "product-import.notifications.missing-column": "Column is missing:",
22
+ "product-import.notifications.no-lines-in-file": "The file is empty.",
23
+ "product-import.notifications.file-read-error": "An error occurred while reading the file.",
24
+ "product-import.notifications.invalid-json": "Invalid JSON structure. Please upload a correct JSON file.",
25
+ "product-import.notifications.missing-languages": "Your CSV file contains languages that are not available in the system. Please add the languages to the system or remove the languages that are not used and try again.",
26
+ "product-import.notifications.mismatched-option-values": "Some product variants are missing product options. Please fill in all options:",
27
+ "product-import.notifications.missing-main-language-name": "Main language is missing for name. Please add the main language and try again.",
28
+ "product-import.notifications.missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported:",
29
+ "product-import.notifications.inconsistent-columns": "The columns in the file are inconsistent. Please ensure all rows have the same number of columns.",
30
+ "product-export.action": "Export products to CSV",
31
+ "product-export.dialog.title": "Export products to CSV",
32
+ "product-export.dialog.label": "File name (optional)",
33
+ "product-export.dialog.export": "Export",
34
+ "product-export.dialog.cancel": "Cancel",
35
+ "product-export.dialog.customFieldsLabel": "Custom fields to include in the export",
36
+ "product-export.dialog.exportAssetsAs": "Export assets as:",
37
+ "product-export.dialog.exportAssetsAsOptions.url": "URL",
38
+ "product-export.dialog.exportAssetsAsOptions.json": "JSON",
39
+ "product-export.dialog.availableExportFieldsLabel": "Fields to export",
40
+ "product-export.dialog.selectAll": "Select all"
41
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "product-import.import-products": "Importera produkter",
3
+ "product-import.card.title": "Importera produkter från CSV",
4
+ "product-import.card.description": "Välj en CSV-fil att importera produkter från",
5
+ "product-import.card.button-select": "Välj fil",
6
+ "product-import.card.button-upload": "Ladda upp fil",
7
+ "product-import.card.update-product-slugs": "Uppdatera produkt-sluggar utifrån namn",
8
+ "product-import.card.select-main-language": "Välj huvudspråk:",
9
+ "product-import.card.updating-strategy": "Uppdateringsstrategi för etiketter och filer:",
10
+ "product-import.card.overwrite": "Skriv över",
11
+ "product-import.card.overwrite-warning": "Varning: Detta kommer ta bort befintliga etiketter och filer från produkter och varianter.",
12
+ "product-import.card.merge": "Sammanfoga",
13
+ "product-import.card.main-language-info": "Huvudspråket används som fallback när översättningar saknas. Det rekommenderas att välja det språk som används mest i din kanal. Huvudspråket kommer användas för att skapa upp produkter, de övriga språken kommer användas för att fylla i översättningar.",
14
+ "product-import.notifications.invalid-file-type": "Felaktig filtyp. Vänligen ladda upp en CSV-fil.",
15
+ "product-import.notifications.invalid-file-structure": "Felaktig filstruktur. Vänligen ladda upp en korrekt CSV-fil.",
16
+ "product-import.notifications.upload-success": "Filen laddades upp och har lagts till i kön för import.",
17
+ "product-import.notifications.upload-error": "Ett fel uppstod vid uppladdning av filen.",
18
+ "product-import.notifications.no-file-selected": "Ingen fil vald.",
19
+ "product-import.notifications.mismatched-values": "Det saknas språkspecifikationer för vissa fält. Vänligen fyll i alla språk:",
20
+ "product-import.notifications.missing-base-columns": "Vissa kolumner saknas:",
21
+ "product-import.notifications.missing-column": "Kolumnen saknas:",
22
+ "product-import.notifications.no-lines-in-file": "Filen är tom.",
23
+ "product-import.notifications.file-read-error": "Ett fel uppstod vid läsning av filen.",
24
+ "product-import.notifications.invalid-json": "Fältet {col} på rad {row} innehåller felaktig JSON-struktur.",
25
+ "product-import.notifications.missing-languages": "Din CSV-fil innehåller språk som inte är tillgängliga i den här kanalen. Vänligen lägg till språken i kanalen eller ta bort de språk som inte används och försök igen.",
26
+ "product-import.notifications.mismatched-option-values": "Antalet produktalternativ för vissa produktvarianter är felaktigt:",
27
+ "product-import.notifications.missing-main-language-name": "Huvudspråket saknas för namn. Vänligen lägg till huvudspråket och försök igen.",
28
+ "product-import.notifications.missing-translation-values": "Det saknas översättningar för vissa fält för huvudspråket som existerar i andra språk. Dessa kommer inte importeras:",
29
+ "product-import.notifications.inconsistent-columns": "Kolumnerna i filen är inkonsekventa. Vänligen kontrollera att alla rader har samma antal kolumner.",
30
+ "product-export.action": "Exportera produkter till CSV",
31
+ "product-export.dialog.title": "Exportera produkter till CSV",
32
+ "product-export.dialog.label": "Filnamn (valfritt)",
33
+ "product-export.dialog.export": "Exportera",
34
+ "product-export.dialog.cancel": "Avbryt",
35
+ "product-export.dialog.customFieldsLabel": "Anpassade fält som ska inkluderas i exporten",
36
+ "product-export.dialog.exportAssetsAs": "Exportera filer som:",
37
+ "product-export.dialog.exportAssetsAsOptions.url": "URL",
38
+ "product-export.dialog.exportAssetsAsOptions.json": "JSON",
39
+ "product-export.dialog.availableExportFieldsLabel": "Fält att exportera",
40
+ "product-export.dialog.selectAll": "Välj alla"
41
+ }
@@ -0,0 +1,64 @@
1
+ // Shared utilities and types for product import/export plugin
2
+
3
+ export type UpdatingStrategy = 'replace' | 'merge'
4
+
5
+ export type ImportOptions = {
6
+ updateProductSlug?: boolean
7
+ }
8
+
9
+ export type ValidateReturnType = {
10
+ isValid: boolean
11
+ langCodes?: (string | undefined)[]
12
+ clearFile?: boolean
13
+ }
14
+
15
+ export type ProductFields =
16
+ | 'productId'
17
+ | 'name'
18
+ | 'slug'
19
+ | 'description'
20
+ | 'assets'
21
+ | 'facets'
22
+ | 'optionGroups'
23
+
24
+ export type VariantFields =
25
+ | 'sku'
26
+ | 'optionValues'
27
+ | 'price'
28
+ | 'taxCategory'
29
+ | 'stockOnHand'
30
+ | 'trackInventory'
31
+ | 'variantAssets'
32
+ | 'variantFacets'
33
+ | 'enabled'
34
+
35
+ export type ExportFields = Array<ProductFields | VariantFields>
36
+
37
+ export interface PluginInitOptions {
38
+ visibleOptions?: Array<keyof ImportOptions>
39
+ defaultOptions?: ImportOptions
40
+ exportOptions?: {
41
+ defaultFileName?: string
42
+ exportAssetsAsOptions?: Array<'url' | 'json'>
43
+ defaultExportAssetsAs?: 'url' | 'json'
44
+ defaultExportFields?: ExportFields
45
+ requiredExportFields?: ExportFields
46
+ }
47
+ }
48
+
49
+ // Helper function to get server location (matches Angular UI's getServerLocation)
50
+ export const getServerLocation = (): string => {
51
+ // In development with Vite, use the API server directly
52
+ if (window.location.port === '5173') {
53
+ return 'http://localhost:3000'
54
+ }
55
+ const { protocol, hostname, port } = window.location
56
+ const origin = port ? `${protocol}//${hostname}:${port}` : `${protocol}//${hostname}`
57
+ return origin
58
+ }
59
+
60
+ // Return vendure-token header if a non-default channel has been selected in the Dashboard
61
+ export const getChannelHeader = (): Record<string, string> => {
62
+ const token = localStorage.getItem('vendure-selected-channel-token')
63
+ return token ? { 'vendure-token': token } : {}
64
+ }
@@ -160,6 +160,7 @@ exports.ProductImportExportPlugin = ProductImportExportPlugin = ProductImportExp
160
160
  });
161
161
  return config;
162
162
  },
163
+ dashboard: './dashboard/index.tsx',
163
164
  compatibility: '^2.0.0 || ^3.0.0',
164
165
  })
165
166
  ], ProductImportExportPlugin);
@@ -1 +1 @@
1
- {"version":3,"file":"product-import-export.plugin.js","sourceRoot":"","sources":["../../../../packages/product-import-export-plugin/src/product-import-export.plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"product-import-export.plugin.js","sourceRoot":"","sources":["../../../../packages/product-import-export-plugin/src/product-import-export.plugin.ts"],"names":[],"mappings":"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"}