@firecms/data_import_export 3.0.0-canary.19 → 3.0.0-canary.191
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/LICENSE +114 -21
- package/README.md +1 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.es.js +12 -994
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +29 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/useImportExportPlugin.d.ts +6 -4
- package/package.json +17 -34
- package/src/index.ts +2 -4
- package/src/useImportExportPlugin.tsx +10 -6
- package/dist/components/DataNewPropertiesMapping.d.ts +0 -15
- package/dist/components/ImportFileUpload.d.ts +0 -3
- package/dist/components/ImportNewPropertyFieldPreview.d.ts +0 -10
- package/dist/components/ImportSaveInProgress.d.ts +0 -8
- package/dist/components/index.d.ts +0 -4
- package/dist/export_import/ExportCollectionAction.d.ts +0 -11
- package/dist/export_import/ImportCollectionAction.d.ts +0 -15
- package/dist/export_import/export.d.ts +0 -10
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/useImportConfig.d.ts +0 -2
- package/dist/types/column_mapping.d.ts +0 -22
- package/dist/types/index.d.ts +0 -1
- package/dist/utils/data.d.ts +0 -11
- package/dist/utils/file_to_json.d.ts +0 -11
- package/dist/utils/get_import_inference_type.d.ts +0 -2
- package/dist/utils/get_properties_mapping.d.ts +0 -3
- package/dist/utils/index.d.ts +0 -4
- package/src/components/DataNewPropertiesMapping.tsx +0 -128
- package/src/components/ImportFileUpload.tsx +0 -34
- package/src/components/ImportNewPropertyFieldPreview.tsx +0 -55
- package/src/components/ImportSaveInProgress.tsx +0 -122
- package/src/components/index.ts +0 -4
- package/src/export_import/ExportCollectionAction.tsx +0 -252
- package/src/export_import/ImportCollectionAction.tsx +0 -442
- package/src/export_import/export.ts +0 -200
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useImportConfig.tsx +0 -28
- package/src/types/column_mapping.ts +0 -32
- package/src/types/index.ts +0 -1
- package/src/utils/data.ts +0 -223
- package/src/utils/file_to_json.ts +0 -88
- package/src/utils/get_import_inference_type.ts +0 -27
- package/src/utils/get_properties_mapping.ts +0 -59
- package/src/utils/index.ts +0 -4
@@ -1,442 +0,0 @@
|
|
1
|
-
import React, { useCallback, useEffect } from "react";
|
2
|
-
import {
|
3
|
-
CollectionActionsProps,
|
4
|
-
EntityCollectionTable,
|
5
|
-
getFieldConfig,
|
6
|
-
getPropertiesWithPropertiesOrder,
|
7
|
-
getPropertyInPath,
|
8
|
-
PropertiesOrBuilders,
|
9
|
-
Property,
|
10
|
-
PropertyConfigBadge,
|
11
|
-
resolveCollection,
|
12
|
-
ResolvedProperties,
|
13
|
-
slugify,
|
14
|
-
useCustomizationController,
|
15
|
-
User,
|
16
|
-
useSelectionController,
|
17
|
-
useSnackbarController
|
18
|
-
} from "@firecms/core";
|
19
|
-
import {
|
20
|
-
Button,
|
21
|
-
cn,
|
22
|
-
defaultBorderMixin,
|
23
|
-
Dialog,
|
24
|
-
DialogActions,
|
25
|
-
DialogContent,
|
26
|
-
FileUploadIcon,
|
27
|
-
IconButton,
|
28
|
-
Select,
|
29
|
-
SelectItem,
|
30
|
-
Tooltip,
|
31
|
-
Typography,
|
32
|
-
} from "@firecms/ui";
|
33
|
-
import { buildEntityPropertiesFromData } from "@firecms/schema_inference";
|
34
|
-
import { useImportConfig } from "../hooks";
|
35
|
-
import { convertDataToEntity, getInferenceType, getPropertiesMapping } from "../utils";
|
36
|
-
import { DataNewPropertiesMapping, ImportFileUpload, ImportSaveInProgress } from "../components";
|
37
|
-
import { ImportConfig } from "../types";
|
38
|
-
|
39
|
-
type ImportState = "initial" | "mapping" | "preview" | "import_data_saving";
|
40
|
-
|
41
|
-
export function ImportCollectionAction<M extends Record<string, any>, UserType extends User>({
|
42
|
-
collection,
|
43
|
-
path,
|
44
|
-
collectionEntitiesCount,
|
45
|
-
onAnalyticsEvent
|
46
|
-
}: CollectionActionsProps<M, UserType> & {
|
47
|
-
onAnalyticsEvent?: (event: string, params?: any) => void;
|
48
|
-
}
|
49
|
-
) {
|
50
|
-
|
51
|
-
const customizationController = useCustomizationController();
|
52
|
-
|
53
|
-
const snackbarController = useSnackbarController();
|
54
|
-
|
55
|
-
const [open, setOpen] = React.useState(false);
|
56
|
-
|
57
|
-
const [step, setStep] = React.useState<ImportState>("initial");
|
58
|
-
|
59
|
-
const importConfig = useImportConfig();
|
60
|
-
|
61
|
-
const handleClickOpen = useCallback(() => {
|
62
|
-
setOpen(true);
|
63
|
-
setStep("initial");
|
64
|
-
}, [setOpen]);
|
65
|
-
|
66
|
-
const handleClose = useCallback(() => {
|
67
|
-
setOpen(false);
|
68
|
-
}, [setOpen]);
|
69
|
-
|
70
|
-
const onMappingComplete = useCallback(() => {
|
71
|
-
setStep("preview");
|
72
|
-
}, []);
|
73
|
-
|
74
|
-
const onPreviewComplete = useCallback(() => {
|
75
|
-
setStep("import_data_saving");
|
76
|
-
}, []);
|
77
|
-
|
78
|
-
const onDataAdded = async (data: object[]) => {
|
79
|
-
importConfig.setImportData(data);
|
80
|
-
|
81
|
-
if (data.length > 0) {
|
82
|
-
const originProperties = await buildEntityPropertiesFromData(data, getInferenceType);
|
83
|
-
importConfig.setOriginProperties(originProperties);
|
84
|
-
|
85
|
-
const headersMapping = buildHeadersMappingFromData(data, collection?.properties);
|
86
|
-
importConfig.setHeadersMapping(headersMapping);
|
87
|
-
const firstKey = Object.keys(headersMapping)?.[0];
|
88
|
-
if (firstKey?.includes("id") || firstKey?.includes("key")) {
|
89
|
-
importConfig.setIdColumn(firstKey);
|
90
|
-
}
|
91
|
-
}
|
92
|
-
setTimeout(() => {
|
93
|
-
setStep("mapping");
|
94
|
-
}, 100);
|
95
|
-
// setStep("mapping");
|
96
|
-
};
|
97
|
-
|
98
|
-
const resolvedCollection = resolveCollection({
|
99
|
-
collection,
|
100
|
-
path,
|
101
|
-
fields: customizationController.propertyConfigs
|
102
|
-
});
|
103
|
-
|
104
|
-
const properties = getPropertiesWithPropertiesOrder<M>(resolvedCollection.properties, resolvedCollection.propertiesOrder as Extract<keyof M, string>[]) as ResolvedProperties<M>;
|
105
|
-
|
106
|
-
const propertiesAndLevel = Object.entries(properties)
|
107
|
-
.flatMap(([key, property]) => getPropertiesAndLevel(key, property, 0));
|
108
|
-
const propertiesOrder = (resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties)) as Extract<keyof M, string>[];
|
109
|
-
if (collection.collectionGroup) {
|
110
|
-
return null;
|
111
|
-
}
|
112
|
-
return <>
|
113
|
-
|
114
|
-
<Tooltip title={"Import"}>
|
115
|
-
<IconButton color={"primary"} onClick={handleClickOpen}>
|
116
|
-
<FileUploadIcon/>
|
117
|
-
</IconButton>
|
118
|
-
</Tooltip>
|
119
|
-
|
120
|
-
<Dialog open={open}
|
121
|
-
fullWidth={step === "preview"}
|
122
|
-
fullHeight={step === "preview"}
|
123
|
-
maxWidth={step === "initial" ? "lg" : "7xl"}>
|
124
|
-
<DialogContent className={"flex flex-col gap-4 my-4"} fullHeight={step === "preview"}>
|
125
|
-
|
126
|
-
{step === "initial" && <>
|
127
|
-
<Typography variant={"h6"}>Import data</Typography>
|
128
|
-
<Typography variant={"body2"}>Upload a CSV, Excel or JSON file and map it to your existing
|
129
|
-
schema</Typography>
|
130
|
-
<ImportFileUpload onDataAdded={onDataAdded}/>
|
131
|
-
</>}
|
132
|
-
|
133
|
-
{step === "mapping" && <>
|
134
|
-
<Typography variant={"h6"}>Map fields</Typography>
|
135
|
-
<DataNewPropertiesMapping headersMapping={importConfig.headersMapping}
|
136
|
-
idColumn={importConfig.idColumn}
|
137
|
-
originProperties={importConfig.originProperties}
|
138
|
-
destinationProperties={properties}
|
139
|
-
onIdPropertyChanged={(value) => importConfig.setIdColumn(value ?? undefined)}
|
140
|
-
buildPropertyView={({
|
141
|
-
isIdColumn,
|
142
|
-
property,
|
143
|
-
propertyKey,
|
144
|
-
importKey
|
145
|
-
}) => {
|
146
|
-
return <PropertyTreeSelect
|
147
|
-
selectedPropertyKey={propertyKey ?? ""}
|
148
|
-
properties={properties}
|
149
|
-
propertiesAndLevel={propertiesAndLevel}
|
150
|
-
isIdColumn={isIdColumn}
|
151
|
-
onIdSelected={() => {
|
152
|
-
importConfig.setIdColumn(importKey);
|
153
|
-
}}
|
154
|
-
onPropertySelected={(newPropertyKey) => {
|
155
|
-
|
156
|
-
const newHeadersMapping: Record<string, string | null> = Object.entries(importConfig.headersMapping)
|
157
|
-
.map(([currentImportKey, currentPropertyKey]) => {
|
158
|
-
if (currentPropertyKey === newPropertyKey) {
|
159
|
-
return { [currentImportKey]: null };
|
160
|
-
}
|
161
|
-
if (currentImportKey === importKey) {
|
162
|
-
return { [currentImportKey]: newPropertyKey };
|
163
|
-
}
|
164
|
-
return { [currentImportKey]: currentPropertyKey };
|
165
|
-
})
|
166
|
-
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
167
|
-
importConfig.setHeadersMapping(newHeadersMapping as Record<string, string>);
|
168
|
-
|
169
|
-
if (newPropertyKey === importConfig.idColumn) {
|
170
|
-
importConfig.setIdColumn(undefined);
|
171
|
-
}
|
172
|
-
|
173
|
-
}}
|
174
|
-
/>;
|
175
|
-
}}/>
|
176
|
-
</>}
|
177
|
-
|
178
|
-
{step === "preview" && <ImportDataPreview importConfig={importConfig}
|
179
|
-
properties={properties}
|
180
|
-
propertiesOrder={propertiesOrder}/>}
|
181
|
-
|
182
|
-
{step === "import_data_saving" && importConfig &&
|
183
|
-
<ImportSaveInProgress importConfig={importConfig}
|
184
|
-
collection={collection}
|
185
|
-
path={path}
|
186
|
-
onImportSuccess={(importedCollection) => {
|
187
|
-
handleClose();
|
188
|
-
snackbarController.open({
|
189
|
-
type: "info",
|
190
|
-
message: "Data imported successfully"
|
191
|
-
});
|
192
|
-
}}
|
193
|
-
/>}
|
194
|
-
|
195
|
-
</DialogContent>
|
196
|
-
<DialogActions>
|
197
|
-
|
198
|
-
{step === "mapping" && <Button onClick={() => setStep("initial")}
|
199
|
-
variant={"text"}>
|
200
|
-
Back
|
201
|
-
</Button>}
|
202
|
-
|
203
|
-
{step === "preview" && <Button onClick={() => setStep("mapping")}
|
204
|
-
variant={"text"}>
|
205
|
-
Back
|
206
|
-
</Button>}
|
207
|
-
|
208
|
-
<Button onClick={handleClose}
|
209
|
-
variant={"text"}>
|
210
|
-
Cancel
|
211
|
-
</Button>
|
212
|
-
|
213
|
-
{step === "mapping" && <Button variant="filled"
|
214
|
-
onClick={onMappingComplete}>
|
215
|
-
Next
|
216
|
-
</Button>}
|
217
|
-
|
218
|
-
{step === "preview" && <Button variant="filled"
|
219
|
-
onClick={onPreviewComplete}>
|
220
|
-
Save data
|
221
|
-
</Button>}
|
222
|
-
|
223
|
-
</DialogActions>
|
224
|
-
</Dialog>
|
225
|
-
|
226
|
-
</>;
|
227
|
-
}
|
228
|
-
|
229
|
-
const internalIDValue = "__internal_id__";
|
230
|
-
|
231
|
-
function PropertyTreeSelect({
|
232
|
-
selectedPropertyKey,
|
233
|
-
properties,
|
234
|
-
onPropertySelected,
|
235
|
-
onIdSelected,
|
236
|
-
propertiesAndLevel,
|
237
|
-
isIdColumn
|
238
|
-
}: {
|
239
|
-
selectedPropertyKey: string | null;
|
240
|
-
properties: Record<string, Property>;
|
241
|
-
onPropertySelected: (propertyKey: string | null) => void;
|
242
|
-
onIdSelected: () => void;
|
243
|
-
propertiesAndLevel: PropertyAndLevel[];
|
244
|
-
isIdColumn?: boolean;
|
245
|
-
}) {
|
246
|
-
|
247
|
-
const selectedProperty = selectedPropertyKey ? getPropertyInPath(properties, selectedPropertyKey) : null;
|
248
|
-
|
249
|
-
const renderValue = useCallback((selectedPropertyKey: string) => {
|
250
|
-
|
251
|
-
if (selectedPropertyKey === internalIDValue) {
|
252
|
-
return <Typography variant={"body2"} className={"p-4"}>Use this column as ID</Typography>;
|
253
|
-
}
|
254
|
-
|
255
|
-
if (!selectedPropertyKey || !selectedProperty) {
|
256
|
-
return <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>;
|
257
|
-
}
|
258
|
-
|
259
|
-
return <PropertySelectEntry propertyKey={selectedPropertyKey}
|
260
|
-
property={selectedProperty as Property}/>;
|
261
|
-
}, [selectedProperty]);
|
262
|
-
|
263
|
-
const onSelectValueChange = useCallback((value: string) => {
|
264
|
-
if (value === internalIDValue) {
|
265
|
-
onIdSelected();
|
266
|
-
onPropertySelected(null);
|
267
|
-
} else if (value === "__do_not_import") {
|
268
|
-
onPropertySelected(null);
|
269
|
-
} else {
|
270
|
-
onPropertySelected(value);
|
271
|
-
}
|
272
|
-
}, []);
|
273
|
-
|
274
|
-
return <Select value={isIdColumn ? internalIDValue : (selectedPropertyKey ?? undefined)}
|
275
|
-
onValueChange={onSelectValueChange}
|
276
|
-
renderValue={renderValue}>
|
277
|
-
|
278
|
-
<SelectItem value={"__do_not_import"}>
|
279
|
-
<Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
|
280
|
-
</SelectItem>
|
281
|
-
|
282
|
-
<SelectItem value={internalIDValue}>
|
283
|
-
<Typography variant={"body2"} className={"p-4"}>Use this column as ID</Typography>
|
284
|
-
</SelectItem>
|
285
|
-
|
286
|
-
{propertiesAndLevel.map(({
|
287
|
-
property,
|
288
|
-
level,
|
289
|
-
propertyKey
|
290
|
-
}) => {
|
291
|
-
return <SelectItem value={propertyKey}
|
292
|
-
key={propertyKey}
|
293
|
-
disabled={property.dataType === "map"}>
|
294
|
-
<PropertySelectEntry propertyKey={propertyKey}
|
295
|
-
property={property}
|
296
|
-
level={level}/>
|
297
|
-
</SelectItem>;
|
298
|
-
})}
|
299
|
-
|
300
|
-
</Select>;
|
301
|
-
}
|
302
|
-
|
303
|
-
type PropertyAndLevel = {
|
304
|
-
property: Property,
|
305
|
-
level: number,
|
306
|
-
propertyKey: string
|
307
|
-
};
|
308
|
-
|
309
|
-
function getPropertiesAndLevel(key: string, property: Property, level: number): PropertyAndLevel[] {
|
310
|
-
const properties: PropertyAndLevel[] = [];
|
311
|
-
properties.push({
|
312
|
-
property,
|
313
|
-
level,
|
314
|
-
propertyKey: key
|
315
|
-
});
|
316
|
-
if (property.dataType === "map" && property.properties) {
|
317
|
-
Object.entries(property.properties).forEach(([childKey, value]) => {
|
318
|
-
properties.push(...getPropertiesAndLevel(`${key}.${childKey}`, value as Property, level + 1));
|
319
|
-
});
|
320
|
-
}
|
321
|
-
return properties;
|
322
|
-
}
|
323
|
-
|
324
|
-
export function PropertySelectEntry({
|
325
|
-
propertyKey,
|
326
|
-
property,
|
327
|
-
level = 0
|
328
|
-
}: {
|
329
|
-
propertyKey: string;
|
330
|
-
property: Property;
|
331
|
-
level?: number;
|
332
|
-
}) {
|
333
|
-
|
334
|
-
const { propertyConfigs } = useCustomizationController();
|
335
|
-
const widget = getFieldConfig(property, propertyConfigs);
|
336
|
-
|
337
|
-
return <div
|
338
|
-
className="flex flex-row w-full text-start items-center h-full">
|
339
|
-
|
340
|
-
{new Array(level).fill(0).map((_, index) =>
|
341
|
-
<div className={cn(defaultBorderMixin, "ml-8 border-l h-12")} key={index}/>)}
|
342
|
-
|
343
|
-
<div className={"m-4"}>
|
344
|
-
<Tooltip title={widget?.name}>
|
345
|
-
<PropertyConfigBadge propertyConfig={widget}/>
|
346
|
-
</Tooltip>
|
347
|
-
</div>
|
348
|
-
|
349
|
-
<div className={"flex flex-col flex-grow p-2 pl-2"}>
|
350
|
-
<Typography variant="body1"
|
351
|
-
component="span"
|
352
|
-
className="flex-grow pr-2">
|
353
|
-
{property.name
|
354
|
-
? property.name
|
355
|
-
: "\u00a0"
|
356
|
-
}
|
357
|
-
</Typography>
|
358
|
-
|
359
|
-
<Typography className=" pr-2"
|
360
|
-
variant={"body2"}
|
361
|
-
component="span"
|
362
|
-
color="secondary">
|
363
|
-
{propertyKey}
|
364
|
-
</Typography>
|
365
|
-
</div>
|
366
|
-
|
367
|
-
</div>;
|
368
|
-
|
369
|
-
}
|
370
|
-
|
371
|
-
export function ImportDataPreview<M extends Record<string, any>>({
|
372
|
-
importConfig,
|
373
|
-
properties,
|
374
|
-
propertiesOrder
|
375
|
-
}: {
|
376
|
-
importConfig: ImportConfig,
|
377
|
-
properties: ResolvedProperties<M>,
|
378
|
-
propertiesOrder: Extract<keyof M, string>[],
|
379
|
-
}) {
|
380
|
-
|
381
|
-
useEffect(() => {
|
382
|
-
const propertiesMapping = getPropertiesMapping(importConfig.originProperties, properties);
|
383
|
-
const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, propertiesMapping, "TEMP_PATH"));
|
384
|
-
importConfig.setEntities(mappedData);
|
385
|
-
}, []);
|
386
|
-
|
387
|
-
const selectionController = useSelectionController();
|
388
|
-
|
389
|
-
return <EntityCollectionTable
|
390
|
-
title={<div>
|
391
|
-
<Typography variant={"subtitle2"}>Imported data preview</Typography>
|
392
|
-
<Typography variant={"caption"}>Entities with the same id will be overwritten</Typography>
|
393
|
-
</div>}
|
394
|
-
tableController={{
|
395
|
-
data: importConfig.entities,
|
396
|
-
dataLoading: false,
|
397
|
-
noMoreToLoad: false
|
398
|
-
}}
|
399
|
-
endAdornment={<div className={"h-12"}/>}
|
400
|
-
filterable={false}
|
401
|
-
sortable={false}
|
402
|
-
selectionController={selectionController}
|
403
|
-
displayedColumnIds={propertiesOrder.map(p => ({
|
404
|
-
key: p,
|
405
|
-
disabled: false
|
406
|
-
}))}
|
407
|
-
properties={properties}/>
|
408
|
-
|
409
|
-
}
|
410
|
-
|
411
|
-
function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders<any>) {
|
412
|
-
const headersMapping: Record<string, string> = {};
|
413
|
-
objArr.filter(Boolean).forEach((obj) => {
|
414
|
-
Object.keys(obj).forEach((key) => {
|
415
|
-
// @ts-ignore
|
416
|
-
const child = obj[key];
|
417
|
-
if (typeof child === "object" && !Array.isArray(child)) {
|
418
|
-
const childProperty = properties?.[key];
|
419
|
-
const childProperties = childProperty && "properties" in childProperty ? childProperty.properties : undefined;
|
420
|
-
const childHeadersMapping = buildHeadersMappingFromData([child], childProperties);
|
421
|
-
Object.entries(childHeadersMapping).forEach(([subKey, mapping]) => {
|
422
|
-
headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
|
423
|
-
});
|
424
|
-
}
|
425
|
-
|
426
|
-
if (!properties) {
|
427
|
-
headersMapping[key] = key;
|
428
|
-
} else if (key in properties) {
|
429
|
-
headersMapping[key] = key;
|
430
|
-
} else {
|
431
|
-
const slug = slugify(key);
|
432
|
-
if (slug in properties) {
|
433
|
-
headersMapping[key] = slug;
|
434
|
-
} else {
|
435
|
-
headersMapping[key] = key;
|
436
|
-
}
|
437
|
-
}
|
438
|
-
|
439
|
-
});
|
440
|
-
});
|
441
|
-
return headersMapping;
|
442
|
-
}
|
@@ -1,200 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
ArrayValuesCount,
|
3
|
-
Entity,
|
4
|
-
EntityReference,
|
5
|
-
getArrayValuesCount,
|
6
|
-
getValueInPath,
|
7
|
-
ResolvedEntityCollection,
|
8
|
-
ResolvedProperties,
|
9
|
-
ResolvedProperty
|
10
|
-
} from "@firecms/core";
|
11
|
-
|
12
|
-
interface Header {
|
13
|
-
key: string;
|
14
|
-
label: string;
|
15
|
-
}
|
16
|
-
|
17
|
-
export function downloadExport<M extends Record<string, any>>(data: Entity<M>[],
|
18
|
-
additionalData: Record<string, any>[] | undefined,
|
19
|
-
collection: ResolvedEntityCollection<M>,
|
20
|
-
flattenArrays: boolean,
|
21
|
-
additionalHeaders: string[] | undefined,
|
22
|
-
exportType: "csv" | "json",
|
23
|
-
dateExportType: "timestamp" | "string"
|
24
|
-
) {
|
25
|
-
|
26
|
-
console.debug("Downloading export", { dataLength: data.length, collection, exportType, dateExportType });
|
27
|
-
const properties = collection.properties;
|
28
|
-
|
29
|
-
if (exportType === "csv") {
|
30
|
-
const arrayValuesCount = flattenArrays ? getArrayValuesCount(data.map(d => d.values)) : {};
|
31
|
-
const headers = getExportHeaders(properties, additionalHeaders, arrayValuesCount);
|
32
|
-
const exportableData = getCSVExportableData(data, additionalData, properties, headers, dateExportType);
|
33
|
-
const headersData = entryToCSVRow(headers.map(h => h.label));
|
34
|
-
const csvData = exportableData.map(entry => entryToCSVRow(entry));
|
35
|
-
downloadBlob([headersData, ...csvData], `${collection.name}.csv`, "text/csv");
|
36
|
-
} else {
|
37
|
-
const exportableData = getJsonExportableData(data, additionalData, properties, dateExportType);
|
38
|
-
const json = JSON.stringify(exportableData, null, 2);
|
39
|
-
downloadBlob([json], `${collection.name}.json`, "application/json");
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
export function getCSVExportableData(data: Entity<any>[],
|
44
|
-
additionalData: Record<string, any>[] | undefined,
|
45
|
-
properties: ResolvedProperties,
|
46
|
-
headers: Header[],
|
47
|
-
dateExportType: "timestamp" | "string"
|
48
|
-
) {
|
49
|
-
|
50
|
-
const mergedData: any[] = data.map(e => ({
|
51
|
-
id: e.id,
|
52
|
-
...processValuesForExport(e.values, properties, "csv", dateExportType)
|
53
|
-
}));
|
54
|
-
|
55
|
-
if (additionalData) {
|
56
|
-
additionalData.forEach((additional, index) => {
|
57
|
-
mergedData[index] = { ...mergedData[index], ...additional };
|
58
|
-
});
|
59
|
-
}
|
60
|
-
|
61
|
-
return mergedData && mergedData.map((entry) => {
|
62
|
-
return headers.map((header) => getValueInPath(entry, header.key));
|
63
|
-
});
|
64
|
-
}
|
65
|
-
|
66
|
-
export function getJsonExportableData(data: Entity<any>[],
|
67
|
-
additionalData: Record<string, any>[] | undefined,
|
68
|
-
properties: ResolvedProperties,
|
69
|
-
dateExportType: "timestamp" | "string"
|
70
|
-
) {
|
71
|
-
|
72
|
-
const mergedData: any[] = data.map(e => ({
|
73
|
-
id: e.id,
|
74
|
-
...processValuesForExport(e.values, properties, "json", dateExportType)
|
75
|
-
}));
|
76
|
-
|
77
|
-
if (additionalData) {
|
78
|
-
additionalData.forEach((additional, index) => {
|
79
|
-
mergedData[index] = { ...mergedData[index], ...additional };
|
80
|
-
});
|
81
|
-
}
|
82
|
-
|
83
|
-
return mergedData;
|
84
|
-
}
|
85
|
-
|
86
|
-
function getExportHeaders<M extends Record<string, any>>(properties: ResolvedProperties<M>,
|
87
|
-
additionalHeaders: string[] | undefined,
|
88
|
-
arrayValuesCount?: ArrayValuesCount): Header[] {
|
89
|
-
|
90
|
-
const headers: Header[] = [
|
91
|
-
{ label: "id", key: "id" },
|
92
|
-
...Object.entries(properties)
|
93
|
-
.flatMap(([childKey, property]) => {
|
94
|
-
if (arrayValuesCount && arrayValuesCount[childKey] > 1) {
|
95
|
-
return Array.from({ length: arrayValuesCount[childKey] },
|
96
|
-
(_, i) => getHeaders(property as ResolvedProperty, `${childKey}[${i}]`, ""))
|
97
|
-
.flat();
|
98
|
-
} else {
|
99
|
-
return getHeaders(property as ResolvedProperty, childKey, "");
|
100
|
-
}
|
101
|
-
})
|
102
|
-
];
|
103
|
-
|
104
|
-
if (additionalHeaders) {
|
105
|
-
headers.push(...additionalHeaders.map(h => ({ label: h, key: h })));
|
106
|
-
}
|
107
|
-
|
108
|
-
return headers;
|
109
|
-
}
|
110
|
-
|
111
|
-
/**
|
112
|
-
* Get headers for property. There could be more than one header per property
|
113
|
-
* @param property
|
114
|
-
* @param propertyKey
|
115
|
-
* @param prefix
|
116
|
-
*/
|
117
|
-
function getHeaders(property: ResolvedProperty, propertyKey: string, prefix = ""): Header[] {
|
118
|
-
const currentKey = prefix ? `${prefix}.${propertyKey}` : propertyKey;
|
119
|
-
if (property.dataType === "map" && property.properties) {
|
120
|
-
return Object.entries(property.properties)
|
121
|
-
.map(([childKey, p]) => getHeaders(p, childKey, currentKey))
|
122
|
-
.flat();
|
123
|
-
} else {
|
124
|
-
return [{ label: currentKey, key: currentKey }];
|
125
|
-
}
|
126
|
-
}
|
127
|
-
|
128
|
-
function processValueForExport(inputValue: any,
|
129
|
-
property: ResolvedProperty,
|
130
|
-
exportType: "csv" | "json",
|
131
|
-
dateExportType: "timestamp" | "string"
|
132
|
-
): any {
|
133
|
-
|
134
|
-
let value;
|
135
|
-
if (property.dataType === "map" && property.properties) {
|
136
|
-
value = processValuesForExport(inputValue, property.properties as ResolvedProperties, exportType, dateExportType);
|
137
|
-
} else if (property.dataType === "array") {
|
138
|
-
if (property.of && Array.isArray(inputValue)) {
|
139
|
-
if (Array.isArray(property.of)) {
|
140
|
-
value = property.of.map((p, i) => processValueForExport(inputValue[i], p, exportType, dateExportType));
|
141
|
-
} else if (property.of.dataType === "map") {
|
142
|
-
value = exportType === "csv"
|
143
|
-
? inputValue.map((e) => JSON.stringify(e))
|
144
|
-
: inputValue.map((e) => processValueForExport(e, property.of as ResolvedProperty, exportType, dateExportType));
|
145
|
-
;
|
146
|
-
} else {
|
147
|
-
value = inputValue.map((e) => processValueForExport(e, property.of as ResolvedProperty, exportType, dateExportType));
|
148
|
-
}
|
149
|
-
} else {
|
150
|
-
value = inputValue;
|
151
|
-
}
|
152
|
-
} else if (property.dataType === "reference" && inputValue && inputValue.isEntityReference && inputValue.isEntityReference()) {
|
153
|
-
const ref = inputValue ? inputValue as EntityReference : undefined;
|
154
|
-
value = ref ? ref.pathWithId : null;
|
155
|
-
} else if (property.dataType === "date" && inputValue instanceof Date) {
|
156
|
-
value = inputValue ? (dateExportType === "timestamp" ? inputValue.getTime() : inputValue.toISOString()) : null;
|
157
|
-
} else {
|
158
|
-
value = inputValue;
|
159
|
-
}
|
160
|
-
|
161
|
-
return value;
|
162
|
-
}
|
163
|
-
|
164
|
-
function processValuesForExport<M extends Record<string, any>>
|
165
|
-
(inputValues: Record<keyof M, any>,
|
166
|
-
properties: ResolvedProperties<M>,
|
167
|
-
exportType: "csv" | "json",
|
168
|
-
dateExportType: "timestamp" | "string"
|
169
|
-
): Record<keyof M, any> {
|
170
|
-
const updatedValues = Object.entries(properties)
|
171
|
-
.map(([key, property]) => {
|
172
|
-
const inputValue = inputValues && (inputValues)[key];
|
173
|
-
const updatedValue = processValueForExport(inputValue, property as ResolvedProperty, exportType, dateExportType);
|
174
|
-
if (updatedValue === undefined) return {};
|
175
|
-
return ({ [key]: updatedValue });
|
176
|
-
})
|
177
|
-
.reduce((a, b) => ({ ...a, ...b }), {}) as Record<keyof M, any>;
|
178
|
-
return { ...inputValues, ...updatedValues };
|
179
|
-
}
|
180
|
-
|
181
|
-
function entryToCSVRow(entry: any[]) {
|
182
|
-
return entry
|
183
|
-
.map((v: any) => {
|
184
|
-
if (v === null || v === undefined) return "";
|
185
|
-
if (Array.isArray(v))
|
186
|
-
return "\"" + JSON.stringify(v).replaceAll("\"", "\\\"") + "\"";
|
187
|
-
const s = String(v);
|
188
|
-
return "\"" + s.replaceAll("\"", "\"\"") + "\"";
|
189
|
-
})
|
190
|
-
.join(",") + "\r\n";
|
191
|
-
}
|
192
|
-
|
193
|
-
export function downloadBlob(content: BlobPart[], filename: string, contentType: string) {
|
194
|
-
const blob = new Blob(content, { type: contentType });
|
195
|
-
const url = URL.createObjectURL(blob);
|
196
|
-
const pom = document.createElement("a");
|
197
|
-
pom.href = url;
|
198
|
-
pom.setAttribute("download", filename);
|
199
|
-
pom.click();
|
200
|
-
}
|
package/src/hooks/index.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export * from "./useImportConfig";
|
@@ -1,28 +0,0 @@
|
|
1
|
-
import { useState } from "react";
|
2
|
-
import { Entity, Property } from "@firecms/core";
|
3
|
-
import { ImportConfig } from "../types";
|
4
|
-
|
5
|
-
export const useImportConfig = (): ImportConfig => {
|
6
|
-
|
7
|
-
const [inUse, setInUse] = useState<boolean>(false);
|
8
|
-
const [idColumn, setIdColumn] = useState<string | undefined>();
|
9
|
-
const [importData, setImportData] = useState<object[]>([]);
|
10
|
-
const [entities, setEntities] = useState<Entity<any>[]>([]);
|
11
|
-
const [headersMapping, setHeadersMapping] = useState<Record<string, string | null>>({});
|
12
|
-
const [originProperties, setOriginProperties] = useState<Record<string, Property>>({});
|
13
|
-
|
14
|
-
return {
|
15
|
-
inUse,
|
16
|
-
setInUse,
|
17
|
-
idColumn,
|
18
|
-
setIdColumn,
|
19
|
-
entities,
|
20
|
-
setEntities,
|
21
|
-
importData,
|
22
|
-
setImportData,
|
23
|
-
headersMapping,
|
24
|
-
setHeadersMapping,
|
25
|
-
originProperties,
|
26
|
-
setOriginProperties,
|
27
|
-
};
|
28
|
-
};
|