@haus-tech/product-import-export-plugin 3.6.3 → 3.6.4
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 +20 -64
- package/{README.mdx → README.md} +62 -35
- package/package.json +9 -9
- package/src/dashboard/bulk-export.tsx +1 -0
- package/src/dashboard/export-configuration-panel.tsx +448 -0
- package/src/dashboard/export-dialog.tsx +22 -285
- package/src/dashboard/export.tsx +52 -17
- package/src/dashboard/exported-list.tsx +176 -96
- package/src/dashboard/import.tsx +286 -149
- package/src/dashboard/index.tsx +27 -28
- package/src/dashboard/use-active-channel-key.ts +7 -0
- package/src/product-import-export.plugin.d.ts +6 -0
- package/src/product-import-export.plugin.js +29 -1
- package/src/product-import-export.plugin.js.map +1 -1
- package/src/providers/import-providers/asset-importer.js +27 -8
- package/src/providers/import-providers/asset-importer.js.map +1 -1
- package/src/providers/import-providers/import-parser.d.ts +1 -0
- package/src/providers/import-providers/import-parser.js +18 -2
- package/src/providers/import-providers/import-parser.js.map +1 -1
- package/src/providers/import-providers/product-importer.d.ts +10 -0
- package/src/providers/import-providers/product-importer.js +111 -20
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/export-storage/export-storage-strategy.d.ts +8 -0
- package/src/services/extended-fast-importer.service.js +20 -12
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/import-storage/import-job-storage-strategy.d.ts +9 -0
- package/src/services/product-export.service.d.ts +20 -6
- package/src/services/product-export.service.js +165 -40
- package/src/services/product-export.service.js.map +1 -1
- package/src/services/self-relation-custom-fields.d.ts +26 -0
- package/src/services/self-relation-custom-fields.js +67 -0
- package/src/services/self-relation-custom-fields.js.map +1 -0
- package/src/types.d.ts +7 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Accordion,
|
|
3
|
+
AccordionContent,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
AccordionTrigger,
|
|
6
|
+
Checkbox,
|
|
7
|
+
Input,
|
|
8
|
+
Label,
|
|
9
|
+
Popover,
|
|
10
|
+
PopoverContent,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
RadioGroup,
|
|
13
|
+
RadioGroupItem,
|
|
14
|
+
toast,
|
|
15
|
+
} from '@vendure/dashboard'
|
|
16
|
+
import { ChevronRight, InfoIcon } from 'lucide-react'
|
|
17
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, forwardRef, useState } from 'react'
|
|
18
|
+
import { useActiveChannelKey } from './use-active-channel-key'
|
|
19
|
+
import {
|
|
20
|
+
getServerLocation,
|
|
21
|
+
getChannelHeader,
|
|
22
|
+
type PluginInitOptions,
|
|
23
|
+
type ExportFields,
|
|
24
|
+
type ProductFields,
|
|
25
|
+
type VariantFields,
|
|
26
|
+
} from './utils'
|
|
27
|
+
|
|
28
|
+
const MANDATORY_OPTION_FIELDS: Array<ProductFields | VariantFields> = [
|
|
29
|
+
'optionGroups',
|
|
30
|
+
'optionValues',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const AVAILABLE_EXPORT_FIELDS: ExportFields = [
|
|
34
|
+
'name',
|
|
35
|
+
'slug',
|
|
36
|
+
'description',
|
|
37
|
+
'id',
|
|
38
|
+
'assets',
|
|
39
|
+
'facets',
|
|
40
|
+
'optionGroups',
|
|
41
|
+
'sku',
|
|
42
|
+
'optionValues',
|
|
43
|
+
'price',
|
|
44
|
+
'taxCategory',
|
|
45
|
+
'stockOnHand',
|
|
46
|
+
'trackInventory',
|
|
47
|
+
'variantAssets',
|
|
48
|
+
'variantFacets',
|
|
49
|
+
'enabled',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export type ExportConfigurationPanelProps = {
|
|
53
|
+
/** Selected product IDs for scoped export; omit when `isExportAll` (treated as empty). */
|
|
54
|
+
productIds?: string[]
|
|
55
|
+
isExportAll: boolean
|
|
56
|
+
/** Fetch config + fields only while true (e.g. dialog `open`) */
|
|
57
|
+
active: boolean
|
|
58
|
+
/** Prefix for input/checkbox ids so modal vs page never collide */
|
|
59
|
+
idPrefix?: string
|
|
60
|
+
/** Expand “Select fields to export” on first paint (e.g. modal); inline page keeps it collapsed */
|
|
61
|
+
fieldsAccordionDefaultOpen?: boolean
|
|
62
|
+
onExportSuccess?: () => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ExportConfigurationPanelHandle = {
|
|
66
|
+
submitExport: () => Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const ExportConfigurationPanel = forwardRef<
|
|
70
|
+
ExportConfigurationPanelHandle,
|
|
71
|
+
ExportConfigurationPanelProps
|
|
72
|
+
>(function ExportConfigurationPanel(
|
|
73
|
+
{
|
|
74
|
+
productIds,
|
|
75
|
+
isExportAll,
|
|
76
|
+
active,
|
|
77
|
+
idPrefix = 'export-config',
|
|
78
|
+
fieldsAccordionDefaultOpen = false,
|
|
79
|
+
onExportSuccess,
|
|
80
|
+
},
|
|
81
|
+
ref,
|
|
82
|
+
) {
|
|
83
|
+
const resolvedProductIds = productIds ?? []
|
|
84
|
+
const activeChannelId = useActiveChannelKey()
|
|
85
|
+
|
|
86
|
+
const [config, setConfig] = useState<PluginInitOptions['exportOptions'] | null>(null)
|
|
87
|
+
|
|
88
|
+
const [fileName, setFileName] = useState('')
|
|
89
|
+
const [customFieldNames, setCustomFieldNames] = useState<string[]>([])
|
|
90
|
+
const [selectedCustomFields, setSelectedCustomFields] = useState<string[]>([])
|
|
91
|
+
const [customColumnNames, setCustomColumnNames] = useState<string[]>([])
|
|
92
|
+
const [selectedCustomColumns, setSelectedCustomColumns] = useState<string[]>([])
|
|
93
|
+
const [exportAssetsAs, setExportAssetsAs] = useState<'url' | 'json'>('url')
|
|
94
|
+
const [selectedExportFields, setSelectedExportFields] = useState<ExportFields>([])
|
|
95
|
+
const [toggleAllChecked, setToggleAllChecked] = useState(false)
|
|
96
|
+
|
|
97
|
+
/** Stable primitive so effects / callbacks don’t churn on fresh array references with the same IDs. */
|
|
98
|
+
const productIdsFetchKey = resolvedProductIds.join(',')
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!active || !activeChannelId) return
|
|
102
|
+
|
|
103
|
+
const fetchInitial = async () => {
|
|
104
|
+
try {
|
|
105
|
+
const serverPath = getServerLocation()
|
|
106
|
+
const [configRes, customFieldsRes] = await Promise.all([
|
|
107
|
+
fetch(`${serverPath}/product-import-export/config`, {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
...getChannelHeader(),
|
|
112
|
+
},
|
|
113
|
+
credentials: 'include',
|
|
114
|
+
}),
|
|
115
|
+
fetch(`${serverPath}/product-export/custom-fields`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
...getChannelHeader(),
|
|
120
|
+
},
|
|
121
|
+
credentials: 'include',
|
|
122
|
+
body: JSON.stringify(resolvedProductIds.length > 0 ? resolvedProductIds : []),
|
|
123
|
+
}),
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
const configData = await configRes.json()
|
|
127
|
+
const customFieldsData = await customFieldsRes.json()
|
|
128
|
+
|
|
129
|
+
setConfig(configData.exportOptions)
|
|
130
|
+
|
|
131
|
+
const names = customFieldsData.map((field: { name: string }) => field.name)
|
|
132
|
+
setCustomFieldNames(names)
|
|
133
|
+
setSelectedCustomFields([...names])
|
|
134
|
+
|
|
135
|
+
const columnNames: string[] =
|
|
136
|
+
configData.exportOptions?.customExportColumns?.map((c: { name: string }) => c.name) ?? []
|
|
137
|
+
setCustomColumnNames(columnNames)
|
|
138
|
+
setSelectedCustomColumns([...columnNames])
|
|
139
|
+
|
|
140
|
+
const defaultAssets =
|
|
141
|
+
configData.exportOptions?.defaultExportAssetsAs ||
|
|
142
|
+
configData.exportOptions?.exportAssetsAsOptions?.[0] ||
|
|
143
|
+
'url'
|
|
144
|
+
setExportAssetsAs(defaultAssets)
|
|
145
|
+
|
|
146
|
+
const initialExportFields = Array.from(
|
|
147
|
+
new Set([
|
|
148
|
+
...(configData.exportOptions?.defaultExportFields || []),
|
|
149
|
+
...(configData.exportOptions?.requiredExportFields || []),
|
|
150
|
+
...MANDATORY_OPTION_FIELDS,
|
|
151
|
+
]),
|
|
152
|
+
) as ExportFields
|
|
153
|
+
|
|
154
|
+
setSelectedExportFields(initialExportFields)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Failed to fetch export config:', error)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fetchInitial()
|
|
161
|
+
}, [active, activeChannelId, productIdsFetchKey])
|
|
162
|
+
|
|
163
|
+
const derivedToggleAll = useMemo(() => {
|
|
164
|
+
const allStandard = AVAILABLE_EXPORT_FIELDS.every((f) => selectedExportFields.includes(f))
|
|
165
|
+
const allCustom =
|
|
166
|
+
customFieldNames.length === 0 ||
|
|
167
|
+
customFieldNames.every((f) => selectedCustomFields.includes(f))
|
|
168
|
+
const allCustomColumns = customColumnNames.every((c) => selectedCustomColumns.includes(c))
|
|
169
|
+
return allStandard && allCustom && allCustomColumns
|
|
170
|
+
}, [
|
|
171
|
+
selectedExportFields,
|
|
172
|
+
selectedCustomFields,
|
|
173
|
+
customFieldNames,
|
|
174
|
+
selectedCustomColumns,
|
|
175
|
+
customColumnNames,
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
setToggleAllChecked(derivedToggleAll)
|
|
180
|
+
}, [derivedToggleAll])
|
|
181
|
+
|
|
182
|
+
const toggleCustomField = (fieldName: string) => {
|
|
183
|
+
setSelectedCustomFields((prev) =>
|
|
184
|
+
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const toggleCustomColumn = (name: string) => {
|
|
189
|
+
setSelectedCustomColumns((prev) =>
|
|
190
|
+
prev.includes(name) ? prev.filter((c) => c !== name) : [...prev, name],
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const toggleExportField = (fieldName: ProductFields | VariantFields) => {
|
|
195
|
+
if (MANDATORY_OPTION_FIELDS.includes(fieldName)) return
|
|
196
|
+
setSelectedExportFields((prev) =>
|
|
197
|
+
prev.includes(fieldName) ? prev.filter((f) => f !== fieldName) : [...prev, fieldName],
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const toggleSelectAll = (checked: boolean) => {
|
|
202
|
+
setToggleAllChecked(checked)
|
|
203
|
+
if (checked) {
|
|
204
|
+
setSelectedExportFields([...AVAILABLE_EXPORT_FIELDS])
|
|
205
|
+
setSelectedCustomFields([...customFieldNames])
|
|
206
|
+
setSelectedCustomColumns([...customColumnNames])
|
|
207
|
+
} else {
|
|
208
|
+
setSelectedExportFields([...(config?.requiredExportFields || []), ...MANDATORY_OPTION_FIELDS])
|
|
209
|
+
setSelectedCustomFields([])
|
|
210
|
+
setSelectedCustomColumns([])
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const bothAssetModes =
|
|
215
|
+
config?.exportAssetsAsOptions?.includes('url') &&
|
|
216
|
+
config?.exportAssetsAsOptions?.includes('json')
|
|
217
|
+
|
|
218
|
+
const submitExport = useCallback(async () => {
|
|
219
|
+
try {
|
|
220
|
+
const serverPath = getServerLocation()
|
|
221
|
+
const trimmed = fileName.trim()
|
|
222
|
+
const finalFileName = trimmed || config?.defaultFileName || 'products_export.csv'
|
|
223
|
+
const endpoint = isExportAll ? 'export-all' : 'export'
|
|
224
|
+
const body = isExportAll ? undefined : JSON.stringify(resolvedProductIds)
|
|
225
|
+
|
|
226
|
+
const res = await fetch(
|
|
227
|
+
`${serverPath}/product-export/${endpoint}?fileName=${encodeURIComponent(
|
|
228
|
+
finalFileName,
|
|
229
|
+
)}&customFields=${encodeURIComponent(
|
|
230
|
+
selectedCustomFields.join(','),
|
|
231
|
+
)}&exportAssetsAs=${exportAssetsAs}&selectedExportFields=${encodeURIComponent(
|
|
232
|
+
[...selectedExportFields, ...selectedCustomColumns].join(','),
|
|
233
|
+
)}`,
|
|
234
|
+
{
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
...getChannelHeader(),
|
|
239
|
+
},
|
|
240
|
+
credentials: 'include',
|
|
241
|
+
...(body && { body }),
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
const json = await res.json().catch(() => ({}))
|
|
247
|
+
throw new Error(json?.message || 'Export failed')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
toast.success(
|
|
251
|
+
isExportAll
|
|
252
|
+
? 'Export queued. You will receive an email when the file is ready.'
|
|
253
|
+
: `Export job queued successfully. ${resolvedProductIds.length} products will be exported.`,
|
|
254
|
+
)
|
|
255
|
+
onExportSuccess?.()
|
|
256
|
+
} catch (error: unknown) {
|
|
257
|
+
console.error(error)
|
|
258
|
+
toast.error((error as Error)?.message || 'Failed to queue export')
|
|
259
|
+
}
|
|
260
|
+
}, [
|
|
261
|
+
fileName,
|
|
262
|
+
config?.defaultFileName,
|
|
263
|
+
isExportAll,
|
|
264
|
+
productIdsFetchKey,
|
|
265
|
+
selectedCustomFields,
|
|
266
|
+
exportAssetsAs,
|
|
267
|
+
selectedExportFields,
|
|
268
|
+
selectedCustomColumns,
|
|
269
|
+
onExportSuccess,
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
useImperativeHandle(ref, () => ({ submitExport }), [submitExport])
|
|
273
|
+
|
|
274
|
+
if (!active) {
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div className="space-y-4">
|
|
280
|
+
<div>
|
|
281
|
+
<Label htmlFor={`${idPrefix}-file-name`}>File name (optional)</Label>
|
|
282
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
283
|
+
A timestamp is added automatically for uniqueness.
|
|
284
|
+
</p>
|
|
285
|
+
<Input
|
|
286
|
+
id={`${idPrefix}-file-name`}
|
|
287
|
+
value={fileName}
|
|
288
|
+
onChange={(e) => setFileName(e.target.value)}
|
|
289
|
+
placeholder={config?.defaultFileName || 'products_export.csv'}
|
|
290
|
+
className="mt-2"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<Accordion
|
|
295
|
+
className="border rounded-lg px-3"
|
|
296
|
+
defaultValue={fieldsAccordionDefaultOpen ? ['select-fields'] : ([] as string[])}
|
|
297
|
+
>
|
|
298
|
+
<AccordionItem value="select-fields" className="border-0">
|
|
299
|
+
<AccordionTrigger className="group/trigger flex w-full items-center gap-3 py-3 text-sm hover:no-underline [&>svg:last-child]:size-4 [&>svg:last-child]:shrink-0 [&>svg:last-child]:self-center">
|
|
300
|
+
<ChevronRight className="text-primary size-4 shrink-0 self-center transition-transform duration-200 group-data-[state=open]/trigger:rotate-90" />
|
|
301
|
+
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-1 self-center text-left leading-snug">
|
|
302
|
+
<span className="font-medium text-foreground">Configure fields to export</span>
|
|
303
|
+
</span>
|
|
304
|
+
</AccordionTrigger>
|
|
305
|
+
<AccordionContent className="space-y-4 pb-4 pt-4 border-t border-border">
|
|
306
|
+
<div className="flex items-center space-x-2 pt-2">
|
|
307
|
+
<Checkbox
|
|
308
|
+
id={`${idPrefix}-select-all`}
|
|
309
|
+
checked={toggleAllChecked}
|
|
310
|
+
onCheckedChange={(checked) => toggleSelectAll(checked === true)}
|
|
311
|
+
/>
|
|
312
|
+
<Label htmlFor={`${idPrefix}-select-all`} className="font-normal">
|
|
313
|
+
Select all fields
|
|
314
|
+
</Label>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
318
|
+
{AVAILABLE_EXPORT_FIELDS.map((field) => (
|
|
319
|
+
<div key={field} className="flex items-center space-x-2">
|
|
320
|
+
<Checkbox
|
|
321
|
+
id={`${idPrefix}-field-${field}`}
|
|
322
|
+
checked={selectedExportFields.includes(field)}
|
|
323
|
+
onCheckedChange={() => toggleExportField(field)}
|
|
324
|
+
disabled={
|
|
325
|
+
config?.requiredExportFields?.includes(field) ||
|
|
326
|
+
MANDATORY_OPTION_FIELDS.includes(field)
|
|
327
|
+
}
|
|
328
|
+
/>
|
|
329
|
+
<Label htmlFor={`${idPrefix}-field-${field}`} className="font-normal">
|
|
330
|
+
{field}
|
|
331
|
+
</Label>
|
|
332
|
+
</div>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
{customFieldNames.length > 0 && (
|
|
337
|
+
<div className="space-y-2 pt-4 border-t">
|
|
338
|
+
<Label className="text-muted-foreground pb-2">Custom fields</Label>
|
|
339
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
340
|
+
{customFieldNames.map((field) => (
|
|
341
|
+
<div key={field} className="flex items-center space-x-2">
|
|
342
|
+
<Checkbox
|
|
343
|
+
id={`${idPrefix}-custom-${field}`}
|
|
344
|
+
checked={selectedCustomFields.includes(field)}
|
|
345
|
+
onCheckedChange={() => toggleCustomField(field)}
|
|
346
|
+
/>
|
|
347
|
+
<Label htmlFor={`${idPrefix}-custom-${field}`} className="font-normal">
|
|
348
|
+
{field}
|
|
349
|
+
</Label>
|
|
350
|
+
</div>
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{customColumnNames.length > 0 && (
|
|
357
|
+
<div className="space-y-2 pt-4 border-t">
|
|
358
|
+
<Label className="text-muted-foreground pb-2">Custom columns</Label>
|
|
359
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
360
|
+
{customColumnNames.map((name) => (
|
|
361
|
+
<div key={name} className="flex items-center space-x-2">
|
|
362
|
+
<Checkbox
|
|
363
|
+
id={`${idPrefix}-custom-column-${name}`}
|
|
364
|
+
checked={selectedCustomColumns.includes(name)}
|
|
365
|
+
onCheckedChange={() => toggleCustomColumn(name)}
|
|
366
|
+
/>
|
|
367
|
+
<Label htmlFor={`${idPrefix}-custom-column-${name}`} className="font-normal">
|
|
368
|
+
{name}
|
|
369
|
+
</Label>
|
|
370
|
+
</div>
|
|
371
|
+
))}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
</AccordionContent>
|
|
376
|
+
</AccordionItem>
|
|
377
|
+
</Accordion>
|
|
378
|
+
|
|
379
|
+
{bothAssetModes ? (
|
|
380
|
+
<Accordion className="border rounded-lg px-3" defaultValue={[]}>
|
|
381
|
+
<AccordionItem value="advanced-settings" className="border-0">
|
|
382
|
+
<AccordionTrigger className="group/trigger flex w-full items-center gap-3 py-3 text-sm hover:no-underline [&>svg:last-child]:size-4 [&>svg:last-child]:shrink-0 [&>svg:last-child]:self-center">
|
|
383
|
+
<ChevronRight className="text-primary size-4 shrink-0 self-center transition-transform duration-200 group-data-[state=open]/trigger:rotate-90" />
|
|
384
|
+
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-1 self-center text-left leading-snug">
|
|
385
|
+
<span className="font-medium text-foreground">Advanced settings</span>
|
|
386
|
+
</span>
|
|
387
|
+
</AccordionTrigger>
|
|
388
|
+
<AccordionContent className="space-y-4 pb-4 pt-4 border-t border-border">
|
|
389
|
+
<div className="flex flex-row flex-wrap items-center gap-x-6 gap-y-2 pt-2">
|
|
390
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
391
|
+
<Label className="font-medium">Export assets as</Label>
|
|
392
|
+
<Popover>
|
|
393
|
+
<PopoverTrigger
|
|
394
|
+
type="button"
|
|
395
|
+
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
396
|
+
aria-label="What does export assets as mean?"
|
|
397
|
+
>
|
|
398
|
+
<InfoIcon className="h-4 w-4" />
|
|
399
|
+
</PopoverTrigger>
|
|
400
|
+
<PopoverContent className="w-80 text-sm" align="start" side="top">
|
|
401
|
+
<p className="font-medium text-foreground">Asset columns in the CSV</p>
|
|
402
|
+
<p className="mt-2 text-muted-foreground">
|
|
403
|
+
Controls how product and variant asset fields are serialized in the export.
|
|
404
|
+
</p>
|
|
405
|
+
<ul className="mt-3 list-disc space-y-2 pl-4 text-muted-foreground">
|
|
406
|
+
<li>
|
|
407
|
+
<span className="font-medium text-foreground">URL only</span> — each cell
|
|
408
|
+
lists asset file URLs separated by{' '}
|
|
409
|
+
<span className="font-mono text-xs">|</span>.
|
|
410
|
+
</li>
|
|
411
|
+
<li>
|
|
412
|
+
<span className="font-medium text-foreground">Full JSON</span> — each cell
|
|
413
|
+
contains JSON with <span className="font-mono text-xs">id</span>,{' '}
|
|
414
|
+
<span className="font-mono text-xs">name</span>, and{' '}
|
|
415
|
+
<span className="font-mono text-xs">url</span> per asset.
|
|
416
|
+
</li>
|
|
417
|
+
</ul>
|
|
418
|
+
</PopoverContent>
|
|
419
|
+
</Popover>
|
|
420
|
+
</div>
|
|
421
|
+
<RadioGroup
|
|
422
|
+
value={exportAssetsAs}
|
|
423
|
+
onValueChange={(val) => setExportAssetsAs(val as 'url' | 'json')}
|
|
424
|
+
className="flex flex-row flex-wrap gap-6"
|
|
425
|
+
>
|
|
426
|
+
<div className="flex items-center space-x-2">
|
|
427
|
+
<RadioGroupItem value="url" id={`${idPrefix}-assets-url`} />
|
|
428
|
+
<Label htmlFor={`${idPrefix}-assets-url`} className="font-normal">
|
|
429
|
+
URL only
|
|
430
|
+
</Label>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex items-center space-x-2">
|
|
433
|
+
<RadioGroupItem value="json" id={`${idPrefix}-assets-json`} />
|
|
434
|
+
<Label htmlFor={`${idPrefix}-assets-json`} className="font-normal">
|
|
435
|
+
Full JSON
|
|
436
|
+
</Label>
|
|
437
|
+
</div>
|
|
438
|
+
</RadioGroup>
|
|
439
|
+
</div>
|
|
440
|
+
</AccordionContent>
|
|
441
|
+
</AccordionItem>
|
|
442
|
+
</Accordion>
|
|
443
|
+
) : null}
|
|
444
|
+
</div>
|
|
445
|
+
)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
ExportConfigurationPanel.displayName = 'ExportConfigurationPanel'
|