@firecms/data_import_export 3.0.0-canary.7 → 3.0.0-canary.70
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 +113 -21
- package/README.md +1 -1
- package/dist/components/DataNewPropertiesMapping.d.ts +3 -5
- package/dist/components/ImportFileUpload.d.ts +1 -1
- package/dist/export_import/BasicExportAction.d.ts +7 -0
- package/dist/export_import/ExportCollectionAction.d.ts +2 -1
- package/dist/export_import/ImportCollectionAction.d.ts +3 -1
- package/dist/export_import/export.d.ts +15 -4
- package/dist/export_import/index.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +954 -554
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/types/column_mapping.d.ts +5 -7
- package/dist/useImportExportPlugin.d.ts +2 -1
- package/dist/utils/data.d.ts +3 -10
- package/dist/utils/file_headers.d.ts +1 -0
- package/dist/utils/file_to_json.d.ts +6 -1
- package/dist/utils/get_properties_mapping.d.ts +0 -3
- package/dist/utils/index.d.ts +0 -1
- package/package.json +25 -23
- package/src/components/DataNewPropertiesMapping.tsx +155 -41
- package/src/components/ImportFileUpload.tsx +12 -4
- package/src/components/ImportNewPropertyFieldPreview.tsx +7 -2
- package/src/components/ImportSaveInProgress.tsx +24 -2
- package/src/export_import/BasicExportAction.tsx +147 -0
- package/src/export_import/ExportCollectionAction.tsx +50 -13
- package/src/export_import/ImportCollectionAction.tsx +54 -33
- package/src/export_import/export.ts +64 -30
- package/src/export_import/index.ts +4 -0
- package/src/hooks/useImportConfig.tsx +6 -0
- package/src/index.ts +1 -0
- package/src/types/column_mapping.ts +6 -6
- package/src/useImportExportPlugin.tsx +4 -3
- package/src/utils/data.ts +50 -127
- package/src/utils/file_headers.ts +90 -0
- package/src/utils/file_to_json.ts +33 -15
- package/src/utils/get_properties_mapping.ts +63 -59
- package/src/utils/index.ts +0 -1
@@ -5,6 +5,7 @@ import {
|
|
5
5
|
Entity,
|
6
6
|
EntityCollection,
|
7
7
|
ExportConfig,
|
8
|
+
getDefaultValuesFor,
|
8
9
|
resolveCollection,
|
9
10
|
ResolvedEntityCollection,
|
10
11
|
useCustomizationController,
|
@@ -18,7 +19,7 @@ import {
|
|
18
19
|
BooleanSwitchWithLabel,
|
19
20
|
Button,
|
20
21
|
CircularProgress,
|
21
|
-
|
22
|
+
cls,
|
22
23
|
Dialog,
|
23
24
|
DialogActions,
|
24
25
|
DialogContent,
|
@@ -26,9 +27,9 @@ import {
|
|
26
27
|
GetAppIcon,
|
27
28
|
IconButton,
|
28
29
|
Tooltip,
|
29
|
-
Typography
|
30
|
+
Typography
|
30
31
|
} from "@firecms/ui";
|
31
|
-
import {
|
32
|
+
import { downloadEntitiesExport } from "./export";
|
32
33
|
|
33
34
|
const DOCS_LIMIT = 500;
|
34
35
|
|
@@ -36,11 +37,13 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
36
37
|
collection: inputCollection,
|
37
38
|
path: inputPath,
|
38
39
|
collectionEntitiesCount,
|
40
|
+
onAnalyticsEvent,
|
39
41
|
exportAllowed,
|
40
42
|
notAllowedView
|
41
43
|
}: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
|
42
44
|
exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: EntityCollection }) => boolean;
|
43
45
|
notAllowedView?: React.ReactNode;
|
46
|
+
onAnalyticsEvent?: (event: string, params?: any) => void;
|
44
47
|
}) {
|
45
48
|
|
46
49
|
const customizationController = useCustomizationController();
|
@@ -48,6 +51,8 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
48
51
|
const exportConfig = typeof inputCollection.exportable === "object" ? inputCollection.exportable : undefined;
|
49
52
|
|
50
53
|
const dateRef = React.useRef<Date>(new Date());
|
54
|
+
|
55
|
+
const [includeUndefinedValues, setIncludeUndefinedValues] = React.useState<boolean>(false);
|
51
56
|
const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);
|
52
57
|
const [exportType, setExportType] = React.useState<"csv" | "json">("csv");
|
53
58
|
const [dateExportType, setDateExportType] = React.useState<"timestamp" | "string">("string");
|
@@ -122,6 +127,9 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
122
127
|
const doDownload = useCallback(async (collection: ResolvedEntityCollection<M>,
|
123
128
|
exportConfig: ExportConfig<any> | undefined) => {
|
124
129
|
|
130
|
+
onAnalyticsEvent?.("export_collection", {
|
131
|
+
collection: collection.path
|
132
|
+
});
|
125
133
|
setDataLoading(true);
|
126
134
|
dataSource.fetchCollection<M>({
|
127
135
|
path,
|
@@ -134,7 +142,30 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
134
142
|
...exportConfig?.additionalFields?.map(column => column.key) ?? [],
|
135
143
|
...collection.additionalFields?.map(field => field.key) ?? []
|
136
144
|
];
|
137
|
-
|
145
|
+
|
146
|
+
const dataWithDefaults = includeUndefinedValues
|
147
|
+
? data.map(entity => {
|
148
|
+
const defaultValues = getDefaultValuesFor(collection.properties);
|
149
|
+
return {
|
150
|
+
...entity,
|
151
|
+
values: { ...defaultValues, ...entity.values }
|
152
|
+
};
|
153
|
+
})
|
154
|
+
: data;
|
155
|
+
downloadEntitiesExport({
|
156
|
+
data: dataWithDefaults,
|
157
|
+
additionalData,
|
158
|
+
properties: collection.properties,
|
159
|
+
propertiesOrder: collection.propertiesOrder,
|
160
|
+
name: collection.name,
|
161
|
+
flattenArrays,
|
162
|
+
additionalHeaders,
|
163
|
+
exportType,
|
164
|
+
dateExportType
|
165
|
+
});
|
166
|
+
onAnalyticsEvent?.("export_collection_success", {
|
167
|
+
collection: collection.path
|
168
|
+
});
|
138
169
|
})
|
139
170
|
.catch((e) => {
|
140
171
|
console.error("Error loading export data", e);
|
@@ -142,7 +173,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
142
173
|
})
|
143
174
|
.finally(() => setDataLoading(false));
|
144
175
|
|
145
|
-
}, [dataSource, path, fetchAdditionalFields, flattenArrays, exportType, dateExportType]);
|
176
|
+
}, [onAnalyticsEvent, dataSource, path, fetchAdditionalFields, includeUndefinedValues, flattenArrays, exportType, dateExportType]);
|
146
177
|
|
147
178
|
const onOkClicked = useCallback(() => {
|
148
179
|
doDownload(collection, exportConfig);
|
@@ -181,17 +212,17 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
181
212
|
<input id="radio-csv" type="radio" value="csv" name="exportType"
|
182
213
|
checked={exportType === "csv"}
|
183
214
|
onChange={() => setExportType("csv")}
|
184
|
-
className={
|
215
|
+
className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
185
216
|
<label htmlFor="radio-csv"
|
186
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
217
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">CSV</label>
|
187
218
|
</div>
|
188
219
|
<div className="flex items-center">
|
189
220
|
<input id="radio-json" type="radio" value="json" name="exportType"
|
190
221
|
checked={exportType === "json"}
|
191
222
|
onChange={() => setExportType("json")}
|
192
|
-
className={
|
223
|
+
className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
193
224
|
<label htmlFor="radio-json"
|
194
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
225
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">JSON</label>
|
195
226
|
</div>
|
196
227
|
</div>
|
197
228
|
|
@@ -200,18 +231,18 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
200
231
|
<input id="radio-timestamp" type="radio" value="timestamp" name="dateExportType"
|
201
232
|
checked={dateExportType === "timestamp"}
|
202
233
|
onChange={() => setDateExportType("timestamp")}
|
203
|
-
className={
|
234
|
+
className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
204
235
|
<label htmlFor="radio-timestamp"
|
205
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
236
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
206
237
|
timestamps ({dateRef.current.getTime()})</label>
|
207
238
|
</div>
|
208
239
|
<div className="flex items-center">
|
209
240
|
<input id="radio-string" type="radio" value="string" name="dateExportType"
|
210
241
|
checked={dateExportType === "string"}
|
211
242
|
onChange={() => setDateExportType("string")}
|
212
|
-
className={
|
243
|
+
className={cls(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
213
244
|
<label htmlFor="radio-string"
|
214
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
245
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
215
246
|
strings ({dateRef.current.toISOString()})</label>
|
216
247
|
</div>
|
217
248
|
</div>
|
@@ -224,6 +255,12 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
224
255
|
onValueChange={setFlattenArrays}
|
225
256
|
label={"Flatten arrays"}/>
|
226
257
|
|
258
|
+
<BooleanSwitchWithLabel
|
259
|
+
size={"small"}
|
260
|
+
value={includeUndefinedValues}
|
261
|
+
onValueChange={setIncludeUndefinedValues}
|
262
|
+
label={"Include undefined values"}/>
|
263
|
+
|
227
264
|
{!canExport && notAllowedView}
|
228
265
|
|
229
266
|
</DialogContent>
|
@@ -2,13 +2,15 @@ import React, { useCallback, useEffect } from "react";
|
|
2
2
|
import {
|
3
3
|
CollectionActionsProps,
|
4
4
|
EntityCollectionTable,
|
5
|
-
PropertyConfigBadge,
|
6
5
|
getFieldConfig,
|
7
6
|
getPropertiesWithPropertiesOrder,
|
8
7
|
getPropertyInPath,
|
8
|
+
PropertiesOrBuilders,
|
9
9
|
Property,
|
10
|
+
PropertyConfigBadge,
|
10
11
|
resolveCollection,
|
11
12
|
ResolvedProperties,
|
13
|
+
slugify,
|
12
14
|
useCustomizationController,
|
13
15
|
User,
|
14
16
|
useSelectionController,
|
@@ -16,7 +18,7 @@ import {
|
|
16
18
|
} from "@firecms/core";
|
17
19
|
import {
|
18
20
|
Button,
|
19
|
-
|
21
|
+
cls,
|
20
22
|
defaultBorderMixin,
|
21
23
|
Dialog,
|
22
24
|
DialogActions,
|
@@ -30,7 +32,7 @@ import {
|
|
30
32
|
} from "@firecms/ui";
|
31
33
|
import { buildEntityPropertiesFromData } from "@firecms/schema_inference";
|
32
34
|
import { useImportConfig } from "../hooks";
|
33
|
-
import { convertDataToEntity, getInferenceType
|
35
|
+
import { convertDataToEntity, getInferenceType } from "../utils";
|
34
36
|
import { DataNewPropertiesMapping, ImportFileUpload, ImportSaveInProgress } from "../components";
|
35
37
|
import { ImportConfig } from "../types";
|
36
38
|
|
@@ -40,8 +42,12 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
40
42
|
collection,
|
41
43
|
path,
|
42
44
|
collectionEntitiesCount,
|
43
|
-
|
45
|
+
onAnalyticsEvent
|
46
|
+
}: CollectionActionsProps<M, UserType> & {
|
47
|
+
onAnalyticsEvent?: (event: string, params?: any) => void;
|
48
|
+
}
|
44
49
|
) {
|
50
|
+
|
45
51
|
const customizationController = useCustomizationController();
|
46
52
|
|
47
53
|
const snackbarController = useSnackbarController();
|
@@ -54,20 +60,23 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
54
60
|
|
55
61
|
const handleClickOpen = useCallback(() => {
|
56
62
|
setOpen(true);
|
63
|
+
onAnalyticsEvent?.("import_open");
|
57
64
|
setStep("initial");
|
58
|
-
}, [
|
65
|
+
}, [onAnalyticsEvent]);
|
59
66
|
|
60
67
|
const handleClose = useCallback(() => {
|
61
68
|
setOpen(false);
|
62
69
|
}, [setOpen]);
|
63
70
|
|
64
71
|
const onMappingComplete = useCallback(() => {
|
72
|
+
onAnalyticsEvent?.("import_mapping_complete");
|
65
73
|
setStep("preview");
|
66
|
-
}, []);
|
74
|
+
}, [onAnalyticsEvent]);
|
67
75
|
|
68
76
|
const onPreviewComplete = useCallback(() => {
|
77
|
+
onAnalyticsEvent?.("import_data_save");
|
69
78
|
setStep("import_data_saving");
|
70
|
-
}, []);
|
79
|
+
}, [onAnalyticsEvent]);
|
71
80
|
|
72
81
|
const onDataAdded = async (data: object[]) => {
|
73
82
|
importConfig.setImportData(data);
|
@@ -76,15 +85,15 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
76
85
|
const originProperties = await buildEntityPropertiesFromData(data, getInferenceType);
|
77
86
|
importConfig.setOriginProperties(originProperties);
|
78
87
|
|
79
|
-
const headersMapping = buildHeadersMappingFromData(data);
|
88
|
+
const headersMapping = buildHeadersMappingFromData(data, collection?.properties);
|
80
89
|
importConfig.setHeadersMapping(headersMapping);
|
81
90
|
const firstKey = Object.keys(headersMapping)?.[0];
|
82
91
|
if (firstKey?.includes("id") || firstKey?.includes("key")) {
|
83
|
-
|
84
|
-
importConfig.setIdColumn(idColumn);
|
92
|
+
importConfig.setIdColumn(firstKey);
|
85
93
|
}
|
86
94
|
}
|
87
95
|
setTimeout(() => {
|
96
|
+
onAnalyticsEvent?.("import_data_added");
|
88
97
|
setStep("mapping");
|
89
98
|
}, 100);
|
90
99
|
// setStep("mapping");
|
@@ -104,6 +113,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
104
113
|
if (collection.collectionGroup) {
|
105
114
|
return null;
|
106
115
|
}
|
116
|
+
|
107
117
|
return <>
|
108
118
|
|
109
119
|
<Tooltip title={"Import"}>
|
@@ -126,17 +136,14 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
126
136
|
</>}
|
127
137
|
|
128
138
|
{step === "mapping" && <>
|
129
|
-
<Typography variant={"h6"}>Map fields</Typography>
|
130
|
-
<DataNewPropertiesMapping
|
131
|
-
idColumn={importConfig.idColumn}
|
132
|
-
originProperties={importConfig.originProperties}
|
139
|
+
<Typography variant={"h6"} className={"ml-3.5"}>Map fields</Typography>
|
140
|
+
<DataNewPropertiesMapping importConfig={importConfig}
|
133
141
|
destinationProperties={properties}
|
134
|
-
onIdPropertyChanged={(value) => importConfig.setIdColumn(value)}
|
135
142
|
buildPropertyView={({
|
136
143
|
isIdColumn,
|
137
144
|
property,
|
138
145
|
propertyKey,
|
139
|
-
importKey
|
146
|
+
importKey,
|
140
147
|
}) => {
|
141
148
|
return <PropertyTreeSelect
|
142
149
|
selectedPropertyKey={propertyKey ?? ""}
|
@@ -148,6 +155,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
148
155
|
}}
|
149
156
|
onPropertySelected={(newPropertyKey) => {
|
150
157
|
|
158
|
+
onAnalyticsEvent?.("import_mapping_field_updated");
|
151
159
|
const newHeadersMapping: Record<string, string | null> = Object.entries(importConfig.headersMapping)
|
152
160
|
.map(([currentImportKey, currentPropertyKey]) => {
|
153
161
|
if (currentPropertyKey === newPropertyKey) {
|
@@ -248,30 +256,31 @@ function PropertyTreeSelect({
|
|
248
256
|
}
|
249
257
|
|
250
258
|
if (!selectedPropertyKey || !selectedProperty) {
|
251
|
-
return <Typography variant={"body2"} className={"p-4"}>Do not import this
|
259
|
+
return <Typography variant={"body2"} color="disabled" className={"p-4"}>Do not import this
|
260
|
+
property</Typography>;
|
252
261
|
}
|
253
262
|
|
254
263
|
return <PropertySelectEntry propertyKey={selectedPropertyKey}
|
255
264
|
property={selectedProperty as Property}/>;
|
256
265
|
}, [selectedProperty]);
|
257
266
|
|
258
|
-
const onSelectValueChange =
|
267
|
+
const onSelectValueChange = (value: string) => {
|
259
268
|
if (value === internalIDValue) {
|
260
269
|
onIdSelected();
|
261
270
|
onPropertySelected(null);
|
262
|
-
} else if (value === "") {
|
271
|
+
} else if (value === "__do_not_import") {
|
263
272
|
onPropertySelected(null);
|
264
273
|
} else {
|
265
274
|
onPropertySelected(value);
|
266
275
|
}
|
267
|
-
}
|
276
|
+
};
|
268
277
|
|
269
278
|
return <Select value={isIdColumn ? internalIDValue : (selectedPropertyKey ?? undefined)}
|
270
279
|
onValueChange={onSelectValueChange}
|
271
280
|
renderValue={renderValue}>
|
272
281
|
|
273
|
-
<SelectItem value={""}>
|
274
|
-
<Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
|
282
|
+
<SelectItem value={"__do_not_import"}>
|
283
|
+
<Typography variant={"body2"} color={"disabled"} className={"p-4"}>Do not import this property</Typography>
|
275
284
|
</SelectItem>
|
276
285
|
|
277
286
|
<SelectItem value={internalIDValue}>
|
@@ -333,7 +342,7 @@ export function PropertySelectEntry({
|
|
333
342
|
className="flex flex-row w-full text-start items-center h-full">
|
334
343
|
|
335
344
|
{new Array(level).fill(0).map((_, index) =>
|
336
|
-
<div className={
|
345
|
+
<div className={cls(defaultBorderMixin, "ml-8 border-l h-12")} key={index}/>)}
|
337
346
|
|
338
347
|
<div className={"m-4"}>
|
339
348
|
<Tooltip title={widget?.name}>
|
@@ -370,12 +379,11 @@ export function ImportDataPreview<M extends Record<string, any>>({
|
|
370
379
|
}: {
|
371
380
|
importConfig: ImportConfig,
|
372
381
|
properties: ResolvedProperties<M>,
|
373
|
-
propertiesOrder: Extract<keyof M, string>[]
|
382
|
+
propertiesOrder: Extract<keyof M, string>[],
|
374
383
|
}) {
|
375
384
|
|
376
385
|
useEffect(() => {
|
377
|
-
const
|
378
|
-
const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, propertiesMapping, "TEMP_PATH"));
|
386
|
+
const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, "TEMP_PATH", importConfig.defaultValues));
|
379
387
|
importConfig.setEntities(mappedData);
|
380
388
|
}, []);
|
381
389
|
|
@@ -391,30 +399,43 @@ export function ImportDataPreview<M extends Record<string, any>>({
|
|
391
399
|
dataLoading: false,
|
392
400
|
noMoreToLoad: false
|
393
401
|
}}
|
402
|
+
enablePopupIcon={false}
|
394
403
|
endAdornment={<div className={"h-12"}/>}
|
395
404
|
filterable={false}
|
396
405
|
sortable={false}
|
397
406
|
selectionController={selectionController}
|
398
|
-
displayedColumnIds={propertiesOrder.map(p => ({
|
399
|
-
key: p,
|
400
|
-
disabled: false
|
401
|
-
}))}
|
402
407
|
properties={properties}/>
|
403
408
|
|
404
409
|
}
|
405
410
|
|
406
|
-
function buildHeadersMappingFromData(objArr: object[]) {
|
411
|
+
function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders<any>) {
|
407
412
|
const headersMapping: Record<string, string> = {};
|
408
413
|
objArr.filter(Boolean).forEach((obj) => {
|
409
414
|
Object.keys(obj).forEach((key) => {
|
410
415
|
// @ts-ignore
|
411
416
|
const child = obj[key];
|
412
417
|
if (typeof child === "object" && !Array.isArray(child)) {
|
413
|
-
|
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]) => {
|
414
422
|
headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
|
415
423
|
});
|
416
424
|
}
|
417
|
-
|
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
|
+
|
418
439
|
});
|
419
440
|
});
|
420
441
|
return headersMapping;
|
@@ -4,7 +4,6 @@ import {
|
|
4
4
|
EntityReference,
|
5
5
|
getArrayValuesCount,
|
6
6
|
getValueInPath,
|
7
|
-
ResolvedEntityCollection,
|
8
7
|
ResolvedProperties,
|
9
8
|
ResolvedProperty
|
10
9
|
} from "@firecms/core";
|
@@ -14,37 +13,57 @@ interface Header {
|
|
14
13
|
label: string;
|
15
14
|
}
|
16
15
|
|
17
|
-
export
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
export interface DownloadEntitiesExportParams<M extends Record<string, any>> {
|
17
|
+
data: Entity<M>[];
|
18
|
+
additionalData: Record<string, any>[] | undefined;
|
19
|
+
properties: ResolvedProperties<M>;
|
20
|
+
propertiesOrder: string[] | undefined;
|
21
|
+
name: string;
|
22
|
+
flattenArrays: boolean;
|
23
|
+
additionalHeaders: string[] | undefined;
|
24
|
+
exportType: "csv" | "json";
|
25
|
+
dateExportType: "timestamp" | "string";
|
26
|
+
}
|
27
|
+
|
28
|
+
export function downloadEntitiesExport<M extends Record<string, any>>({
|
29
|
+
data,
|
30
|
+
additionalData,
|
31
|
+
properties,
|
32
|
+
propertiesOrder,
|
33
|
+
name,
|
34
|
+
flattenArrays,
|
35
|
+
additionalHeaders,
|
36
|
+
exportType,
|
37
|
+
dateExportType
|
38
|
+
}: DownloadEntitiesExportParams<M>
|
24
39
|
) {
|
25
40
|
|
26
|
-
console.debug("Downloading export", {
|
27
|
-
|
41
|
+
console.debug("Downloading export", {
|
42
|
+
dataLength: data.length,
|
43
|
+
properties,
|
44
|
+
exportType,
|
45
|
+
dateExportType
|
46
|
+
});
|
28
47
|
|
29
48
|
if (exportType === "csv") {
|
30
49
|
const arrayValuesCount = flattenArrays ? getArrayValuesCount(data.map(d => d.values)) : {};
|
31
|
-
const headers = getExportHeaders(properties, additionalHeaders, arrayValuesCount);
|
32
|
-
const exportableData =
|
50
|
+
const headers = getExportHeaders(properties, propertiesOrder, additionalHeaders, arrayValuesCount);
|
51
|
+
const exportableData = getEntityCSVExportableData(data, additionalData, properties, headers, dateExportType);
|
33
52
|
const headersData = entryToCSVRow(headers.map(h => h.label));
|
34
53
|
const csvData = exportableData.map(entry => entryToCSVRow(entry));
|
35
|
-
downloadBlob([headersData, ...csvData], `${
|
54
|
+
downloadBlob([headersData, ...csvData], `${name}.csv`, "text/csv");
|
36
55
|
} else {
|
37
|
-
const exportableData =
|
56
|
+
const exportableData = getEntityJsonExportableData(data, additionalData, properties, dateExportType);
|
38
57
|
const json = JSON.stringify(exportableData, null, 2);
|
39
|
-
downloadBlob([json], `${
|
58
|
+
downloadBlob([json], `${name}.json`, "application/json");
|
40
59
|
}
|
41
60
|
}
|
42
61
|
|
43
|
-
export function
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
62
|
+
export function getEntityCSVExportableData(data: Entity<any>[],
|
63
|
+
additionalData: Record<string, any>[] | undefined,
|
64
|
+
properties: ResolvedProperties,
|
65
|
+
headers: Header[],
|
66
|
+
dateExportType: "timestamp" | "string"
|
48
67
|
) {
|
49
68
|
|
50
69
|
const mergedData: any[] = data.map(e => ({
|
@@ -63,10 +82,10 @@ export function getCSVExportableData(data: Entity<any>[],
|
|
63
82
|
});
|
64
83
|
}
|
65
84
|
|
66
|
-
export function
|
67
|
-
|
68
|
-
|
69
|
-
|
85
|
+
export function getEntityJsonExportableData(data: Entity<any>[],
|
86
|
+
additionalData: Record<string, any>[] | undefined,
|
87
|
+
properties: ResolvedProperties,
|
88
|
+
dateExportType: "timestamp" | "string"
|
70
89
|
) {
|
71
90
|
|
72
91
|
const mergedData: any[] = data.map(e => ({
|
@@ -84,13 +103,22 @@ export function getJsonExportableData(data: Entity<any>[],
|
|
84
103
|
}
|
85
104
|
|
86
105
|
function getExportHeaders<M extends Record<string, any>>(properties: ResolvedProperties<M>,
|
106
|
+
propertiesOrder: string[] | undefined,
|
87
107
|
additionalHeaders: string[] | undefined,
|
88
108
|
arrayValuesCount?: ArrayValuesCount): Header[] {
|
89
109
|
|
90
110
|
const headers: Header[] = [
|
91
|
-
{
|
92
|
-
|
93
|
-
|
111
|
+
{
|
112
|
+
label: "id",
|
113
|
+
key: "id"
|
114
|
+
},
|
115
|
+
...(propertiesOrder ?? Object.keys(properties))
|
116
|
+
.flatMap((childKey) => {
|
117
|
+
const property = properties[childKey];
|
118
|
+
if (!property) {
|
119
|
+
console.warn("Property not found", childKey, properties);
|
120
|
+
return [];
|
121
|
+
}
|
94
122
|
if (arrayValuesCount && arrayValuesCount[childKey] > 1) {
|
95
123
|
return Array.from({ length: arrayValuesCount[childKey] },
|
96
124
|
(_, i) => getHeaders(property as ResolvedProperty, `${childKey}[${i}]`, ""))
|
@@ -102,7 +130,10 @@ function getExportHeaders<M extends Record<string, any>>(properties: ResolvedPro
|
|
102
130
|
];
|
103
131
|
|
104
132
|
if (additionalHeaders) {
|
105
|
-
headers.push(...additionalHeaders.map(h => ({
|
133
|
+
headers.push(...additionalHeaders.map(h => ({
|
134
|
+
label: h,
|
135
|
+
key: h
|
136
|
+
})));
|
106
137
|
}
|
107
138
|
|
108
139
|
return headers;
|
@@ -121,7 +152,10 @@ function getHeaders(property: ResolvedProperty, propertyKey: string, prefix = ""
|
|
121
152
|
.map(([childKey, p]) => getHeaders(p, childKey, currentKey))
|
122
153
|
.flat();
|
123
154
|
} else {
|
124
|
-
return [{
|
155
|
+
return [{
|
156
|
+
label: currentKey,
|
157
|
+
key: currentKey
|
158
|
+
}];
|
125
159
|
}
|
126
160
|
}
|
127
161
|
|
@@ -149,7 +183,7 @@ function processValueForExport(inputValue: any,
|
|
149
183
|
} else {
|
150
184
|
value = inputValue;
|
151
185
|
}
|
152
|
-
} else if (property.dataType === "reference" && inputValue.isEntityReference && inputValue.isEntityReference()) {
|
186
|
+
} else if (property.dataType === "reference" && inputValue && inputValue.isEntityReference && inputValue.isEntityReference()) {
|
153
187
|
const ref = inputValue ? inputValue as EntityReference : undefined;
|
154
188
|
value = ref ? ref.pathWithId : null;
|
155
189
|
} else if (property.dataType === "date" && inputValue instanceof Date) {
|
@@ -5,10 +5,12 @@ import { ImportConfig } from "../types";
|
|
5
5
|
export const useImportConfig = (): ImportConfig => {
|
6
6
|
|
7
7
|
const [inUse, setInUse] = useState<boolean>(false);
|
8
|
+
const [defaultValues, setDefaultValues] = useState<Record<string, any>>({});
|
8
9
|
const [idColumn, setIdColumn] = useState<string | undefined>();
|
9
10
|
const [importData, setImportData] = useState<object[]>([]);
|
10
11
|
const [entities, setEntities] = useState<Entity<any>[]>([]);
|
11
12
|
const [headersMapping, setHeadersMapping] = useState<Record<string, string | null>>({});
|
13
|
+
const [headingsOrder, setHeadingsOrder] = useState<string[]>([]);
|
12
14
|
const [originProperties, setOriginProperties] = useState<Record<string, Property>>({});
|
13
15
|
|
14
16
|
return {
|
@@ -20,9 +22,13 @@ export const useImportConfig = (): ImportConfig => {
|
|
20
22
|
setEntities,
|
21
23
|
importData,
|
22
24
|
setImportData,
|
25
|
+
headingsOrder: (headingsOrder ?? []).length > 0 ? headingsOrder : Object.keys(headersMapping),
|
26
|
+
setHeadingsOrder,
|
23
27
|
headersMapping,
|
24
28
|
setHeadersMapping,
|
25
29
|
originProperties,
|
26
30
|
setOriginProperties,
|
31
|
+
defaultValues,
|
32
|
+
setDefaultValues
|
27
33
|
};
|
28
34
|
};
|
package/src/index.ts
CHANGED
@@ -22,11 +22,11 @@ export type ImportConfig = {
|
|
22
22
|
originProperties: Record<string, Property>;
|
23
23
|
setOriginProperties: React.Dispatch<React.SetStateAction<Record<string, Property>>>;
|
24
24
|
|
25
|
-
|
25
|
+
// unmapped headings order
|
26
|
+
headingsOrder: string[];
|
27
|
+
setHeadingsOrder: React.Dispatch<React.SetStateAction<string[]>>;
|
28
|
+
|
29
|
+
defaultValues: Record<string, any>;
|
30
|
+
setDefaultValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
26
31
|
|
27
|
-
export type DataTypeMapping = {
|
28
|
-
from: DataType;
|
29
|
-
fromSubtype?: DataType;
|
30
|
-
to: DataType;
|
31
|
-
toSubtype?: DataType;
|
32
32
|
}
|
@@ -9,8 +9,8 @@ import { ExportCollectionAction } from "./export_import/ExportCollectionAction";
|
|
9
9
|
export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSPlugin<any, any, any, ImportExportPluginProps> {
|
10
10
|
|
11
11
|
return useMemo(() => ({
|
12
|
-
|
13
|
-
|
12
|
+
key: "import_export",
|
13
|
+
collectionView: {
|
14
14
|
CollectionActions: [ImportCollectionAction, ExportCollectionAction],
|
15
15
|
collectionActionsProps: props
|
16
16
|
}
|
@@ -19,6 +19,7 @@ export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSP
|
|
19
19
|
|
20
20
|
export type ImportExportPluginProps = {
|
21
21
|
exportAllowed?: (props: ExportAllowedParams) => boolean;
|
22
|
-
notAllowedView
|
22
|
+
notAllowedView?: React.ReactNode;
|
23
|
+
onAnalyticsEvent?: (event: string, params?: any) => void;
|
23
24
|
}
|
24
25
|
export type ExportAllowedParams = { collectionEntitiesCount: number, path: string, collection: EntityCollection };
|