@firecms/data_import_export 3.0.0-canary.4 → 3.0.0-canary.41
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/README.md +1 -1
- package/dist/components/DataNewPropertiesMapping.d.ts +1 -1
- package/dist/components/ImportSaveInProgress.d.ts +2 -1
- package/dist/export_import/ExportCollectionAction.d.ts +1 -0
- package/dist/export_import/ImportCollectionAction.d.ts +3 -1
- package/dist/index.es.js +503 -473
- 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/useImportExportPlugin.d.ts +1 -0
- package/package.json +17 -17
- package/src/components/DataNewPropertiesMapping.tsx +5 -4
- package/src/components/ImportSaveInProgress.tsx +31 -4
- package/src/export_import/ExportCollectionAction.tsx +5 -4
- package/src/export_import/ImportCollectionAction.tsx +34 -12
- package/src/export_import/export.ts +1 -1
- package/src/useImportExportPlugin.tsx +3 -2
- package/src/utils/data.ts +14 -1
- package/src/utils/file_to_json.ts +10 -5
@@ -16,7 +16,7 @@ export interface DataPropertyMappingProps {
|
|
16
16
|
headersMapping: Record<string, string | null>;
|
17
17
|
originProperties: Record<string, Property>;
|
18
18
|
destinationProperties: Record<string, Property>;
|
19
|
-
onIdPropertyChanged: (value: string) => void;
|
19
|
+
onIdPropertyChanged: (value: string | null) => void;
|
20
20
|
buildPropertyView?: (props: {
|
21
21
|
isIdColumn: boolean,
|
22
22
|
property: Property | null,
|
@@ -103,14 +103,15 @@ function IdSelectField({
|
|
103
103
|
}: {
|
104
104
|
idColumn?: string,
|
105
105
|
headersMapping: Record<string, string | null>;
|
106
|
-
onChange: (value: string) => void
|
106
|
+
onChange: (value: string | null) => void
|
107
107
|
}) {
|
108
108
|
return <div>
|
109
109
|
<Select
|
110
110
|
size={"small"}
|
111
111
|
value={idColumn ?? ""}
|
112
112
|
onChange={(event) => {
|
113
|
-
|
113
|
+
const value = event.target.value;
|
114
|
+
onChange(value === "none" ? null : value);
|
114
115
|
}}
|
115
116
|
renderValue={(value) => {
|
116
117
|
return <Typography variant={"body2"}>
|
@@ -118,7 +119,7 @@ function IdSelectField({
|
|
118
119
|
</Typography>;
|
119
120
|
}}
|
120
121
|
label={"Column that will be used as ID for each document"}>
|
121
|
-
<SelectItem value={""}>Autogenerate ID</SelectItem>
|
122
|
+
<SelectItem value={"none"}>Autogenerate ID</SelectItem>
|
122
123
|
{Object.entries(headersMapping).map(([key, value]) => {
|
123
124
|
return <SelectItem key={key} value={key}>{key}</SelectItem>;
|
124
125
|
})}
|
@@ -1,20 +1,23 @@
|
|
1
|
-
import {
|
2
|
-
import { CenteredView, CircularProgress, Typography, } from "@firecms/ui";
|
1
|
+
import { DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
|
2
|
+
import { Button, CenteredView, CircularProgress, Typography, } from "@firecms/ui";
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
4
4
|
import { ImportConfig } from "../types";
|
5
5
|
|
6
6
|
export function ImportSaveInProgress<C extends EntityCollection>
|
7
7
|
({
|
8
|
+
path,
|
8
9
|
importConfig,
|
9
10
|
collection,
|
10
11
|
onImportSuccess
|
11
12
|
}:
|
12
13
|
{
|
14
|
+
path: string,
|
13
15
|
importConfig: ImportConfig,
|
14
16
|
collection: C,
|
15
17
|
onImportSuccess: (collection: C) => void
|
16
18
|
}) {
|
17
19
|
|
20
|
+
const [errorSaving, setErrorSaving] = useState<Error | undefined>(undefined);
|
18
21
|
const dataSource = useDataSource();
|
19
22
|
|
20
23
|
const savingRef = useRef<boolean>(false);
|
@@ -31,6 +34,7 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
31
34
|
saveDataBatch(
|
32
35
|
dataSource,
|
33
36
|
collection,
|
37
|
+
path,
|
34
38
|
importConfig.entities,
|
35
39
|
0,
|
36
40
|
25,
|
@@ -38,6 +42,9 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
38
42
|
).then(() => {
|
39
43
|
onImportSuccess(collection);
|
40
44
|
savingRef.current = false;
|
45
|
+
}).catch((e) => {
|
46
|
+
setErrorSaving(e);
|
47
|
+
savingRef.current = false;
|
41
48
|
});
|
42
49
|
}
|
43
50
|
|
@@ -45,6 +52,25 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
45
52
|
save();
|
46
53
|
}, []);
|
47
54
|
|
55
|
+
if (errorSaving) {
|
56
|
+
return (
|
57
|
+
<CenteredView className={"flex flex-col gap-4 items-center"}>
|
58
|
+
<Typography variant={"h6"}>
|
59
|
+
Error saving data
|
60
|
+
</Typography>
|
61
|
+
|
62
|
+
<Typography variant={"body2"} color={"error"}>
|
63
|
+
{errorSaving.message}
|
64
|
+
</Typography>
|
65
|
+
<Button
|
66
|
+
onClick={save}
|
67
|
+
variant={"outlined"}>
|
68
|
+
Retry
|
69
|
+
</Button>
|
70
|
+
</CenteredView>
|
71
|
+
);
|
72
|
+
}
|
73
|
+
|
48
74
|
return (
|
49
75
|
<CenteredView className={"flex flex-col gap-4 items-center"}>
|
50
76
|
<CircularProgress/>
|
@@ -68,6 +94,7 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
68
94
|
|
69
95
|
function saveDataBatch(dataSource: DataSource,
|
70
96
|
collection: EntityCollection,
|
97
|
+
path: string,
|
71
98
|
data: Partial<Entity<any>>[],
|
72
99
|
offset = 0,
|
73
100
|
batchSize = 25,
|
@@ -78,7 +105,7 @@ function saveDataBatch(dataSource: DataSource,
|
|
78
105
|
const batch = data.slice(offset, offset + batchSize);
|
79
106
|
return Promise.all(batch.map(d =>
|
80
107
|
dataSource.saveEntity({
|
81
|
-
path
|
108
|
+
path,
|
82
109
|
values: d.values,
|
83
110
|
entityId: d.id,
|
84
111
|
collection,
|
@@ -87,7 +114,7 @@ function saveDataBatch(dataSource: DataSource,
|
|
87
114
|
.then(() => {
|
88
115
|
if (offset + batchSize < data.length) {
|
89
116
|
onProgressUpdate(offset + batchSize);
|
90
|
-
return saveDataBatch(dataSource, collection, data, offset + batchSize, batchSize, onProgressUpdate);
|
117
|
+
return saveDataBatch(dataSource, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
|
91
118
|
}
|
92
119
|
onProgressUpdate(data.length);
|
93
120
|
return Promise.resolve();
|
@@ -41,6 +41,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
41
41
|
}: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
|
42
42
|
exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: EntityCollection }) => boolean;
|
43
43
|
notAllowedView?: React.ReactNode;
|
44
|
+
onAnalyticsEvent?: (event: string, params?: any) => void;
|
44
45
|
}) {
|
45
46
|
|
46
47
|
const customizationController = useCustomizationController();
|
@@ -183,7 +184,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
183
184
|
onChange={() => setExportType("csv")}
|
184
185
|
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
185
186
|
<label htmlFor="radio-csv"
|
186
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
187
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">CSV</label>
|
187
188
|
</div>
|
188
189
|
<div className="flex items-center">
|
189
190
|
<input id="radio-json" type="radio" value="json" name="exportType"
|
@@ -191,7 +192,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
191
192
|
onChange={() => setExportType("json")}
|
192
193
|
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
193
194
|
<label htmlFor="radio-json"
|
194
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
195
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">JSON</label>
|
195
196
|
</div>
|
196
197
|
</div>
|
197
198
|
|
@@ -202,7 +203,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
202
203
|
onChange={() => setDateExportType("timestamp")}
|
203
204
|
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
204
205
|
<label htmlFor="radio-timestamp"
|
205
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
206
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
206
207
|
timestamps ({dateRef.current.getTime()})</label>
|
207
208
|
</div>
|
208
209
|
<div className="flex items-center">
|
@@ -211,7 +212,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
|
|
211
212
|
onChange={() => setDateExportType("string")}
|
212
213
|
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
213
214
|
<label htmlFor="radio-string"
|
214
|
-
className="p-2 text-sm font-medium text-gray-900 dark:text-
|
215
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
215
216
|
strings ({dateRef.current.toISOString()})</label>
|
216
217
|
</div>
|
217
218
|
</div>
|
@@ -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,
|
@@ -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();
|
@@ -76,12 +82,11 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
76
82
|
const originProperties = await buildEntityPropertiesFromData(data, getInferenceType);
|
77
83
|
importConfig.setOriginProperties(originProperties);
|
78
84
|
|
79
|
-
const headersMapping = buildHeadersMappingFromData(data);
|
85
|
+
const headersMapping = buildHeadersMappingFromData(data, collection?.properties);
|
80
86
|
importConfig.setHeadersMapping(headersMapping);
|
81
87
|
const firstKey = Object.keys(headersMapping)?.[0];
|
82
88
|
if (firstKey?.includes("id") || firstKey?.includes("key")) {
|
83
|
-
|
84
|
-
importConfig.setIdColumn(idColumn);
|
89
|
+
importConfig.setIdColumn(firstKey);
|
85
90
|
}
|
86
91
|
}
|
87
92
|
setTimeout(() => {
|
@@ -131,7 +136,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
131
136
|
idColumn={importConfig.idColumn}
|
132
137
|
originProperties={importConfig.originProperties}
|
133
138
|
destinationProperties={properties}
|
134
|
-
onIdPropertyChanged={(value) => importConfig.setIdColumn(value)}
|
139
|
+
onIdPropertyChanged={(value) => importConfig.setIdColumn(value ?? undefined)}
|
135
140
|
buildPropertyView={({
|
136
141
|
isIdColumn,
|
137
142
|
property,
|
@@ -177,6 +182,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
177
182
|
{step === "import_data_saving" && importConfig &&
|
178
183
|
<ImportSaveInProgress importConfig={importConfig}
|
179
184
|
collection={collection}
|
185
|
+
path={path}
|
180
186
|
onImportSuccess={(importedCollection) => {
|
181
187
|
handleClose();
|
182
188
|
snackbarController.open({
|
@@ -258,7 +264,7 @@ function PropertyTreeSelect({
|
|
258
264
|
if (value === internalIDValue) {
|
259
265
|
onIdSelected();
|
260
266
|
onPropertySelected(null);
|
261
|
-
} else if (value === "") {
|
267
|
+
} else if (value === "__do_not_import") {
|
262
268
|
onPropertySelected(null);
|
263
269
|
} else {
|
264
270
|
onPropertySelected(value);
|
@@ -269,7 +275,7 @@ function PropertyTreeSelect({
|
|
269
275
|
onValueChange={onSelectValueChange}
|
270
276
|
renderValue={renderValue}>
|
271
277
|
|
272
|
-
<SelectItem value={""}>
|
278
|
+
<SelectItem value={"__do_not_import"}>
|
273
279
|
<Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
|
274
280
|
</SelectItem>
|
275
281
|
|
@@ -369,7 +375,7 @@ export function ImportDataPreview<M extends Record<string, any>>({
|
|
369
375
|
}: {
|
370
376
|
importConfig: ImportConfig,
|
371
377
|
properties: ResolvedProperties<M>,
|
372
|
-
propertiesOrder: Extract<keyof M, string>[]
|
378
|
+
propertiesOrder: Extract<keyof M, string>[],
|
373
379
|
}) {
|
374
380
|
|
375
381
|
useEffect(() => {
|
@@ -402,18 +408,34 @@ export function ImportDataPreview<M extends Record<string, any>>({
|
|
402
408
|
|
403
409
|
}
|
404
410
|
|
405
|
-
function buildHeadersMappingFromData(objArr: object[]) {
|
411
|
+
function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders<any>) {
|
406
412
|
const headersMapping: Record<string, string> = {};
|
407
413
|
objArr.filter(Boolean).forEach((obj) => {
|
408
414
|
Object.keys(obj).forEach((key) => {
|
409
415
|
// @ts-ignore
|
410
416
|
const child = obj[key];
|
411
417
|
if (typeof child === "object" && !Array.isArray(child)) {
|
412
|
-
|
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]) => {
|
413
422
|
headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
|
414
423
|
});
|
415
424
|
}
|
416
|
-
|
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
|
+
|
417
439
|
});
|
418
440
|
});
|
419
441
|
return headersMapping;
|
@@ -149,7 +149,7 @@ function processValueForExport(inputValue: any,
|
|
149
149
|
} else {
|
150
150
|
value = inputValue;
|
151
151
|
}
|
152
|
-
} else if (property.dataType === "reference" && inputValue.isEntityReference && inputValue.isEntityReference()) {
|
152
|
+
} else if (property.dataType === "reference" && inputValue && inputValue.isEntityReference && inputValue.isEntityReference()) {
|
153
153
|
const ref = inputValue ? inputValue as EntityReference : undefined;
|
154
154
|
value = ref ? ref.pathWithId : null;
|
155
155
|
} else if (property.dataType === "date" && inputValue instanceof Date) {
|
@@ -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
|
}
|
@@ -20,5 +20,6 @@ export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSP
|
|
20
20
|
export type ImportExportPluginProps = {
|
21
21
|
exportAllowed?: (props: ExportAllowedParams) => boolean;
|
22
22
|
notAllowedView: React.ReactNode;
|
23
|
+
onAnalyticsEvent?: (event: string, params?: any) => void;
|
23
24
|
}
|
24
25
|
export type ExportAllowedParams = { collectionEntitiesCount: number, path: string, collection: EntityCollection };
|
package/src/utils/data.ts
CHANGED
@@ -36,8 +36,21 @@ export function convertDataToEntity(data: Record<any, any>,
|
|
36
36
|
})
|
37
37
|
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
38
38
|
const values = unflattenObject(mappedKeysObject);
|
39
|
+
let id = idColumn ? data[idColumn] : undefined;
|
40
|
+
if (typeof id === "string") {
|
41
|
+
id = id.trim();
|
42
|
+
} else if (typeof id === "number") {
|
43
|
+
id = id.toString();
|
44
|
+
} else if (typeof id === "boolean") {
|
45
|
+
id = id.toString();
|
46
|
+
} else if (id instanceof Date) {
|
47
|
+
id = id.toISOString();
|
48
|
+
} else if (id && "toString" in id) {
|
49
|
+
id = id.toString();
|
50
|
+
}
|
51
|
+
|
39
52
|
return {
|
40
|
-
id
|
53
|
+
id,
|
41
54
|
values,
|
42
55
|
path
|
43
56
|
};
|
@@ -7,12 +7,17 @@ export function convertFileToJson(file: File): Promise<object[]> {
|
|
7
7
|
console.debug("Converting JSON file to JSON", file.name);
|
8
8
|
const reader = new FileReader();
|
9
9
|
reader.onload = function (e) {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
try {
|
11
|
+
const data = e.target?.result as string;
|
12
|
+
const jsonData = JSON.parse(data);
|
13
|
+
if (!Array.isArray(jsonData)) {
|
14
|
+
reject(new Error("JSON file should contain an array of objects"));
|
15
|
+
}
|
16
|
+
resolve(jsonData);
|
17
|
+
} catch (e) {
|
18
|
+
console.error("Error parsing JSON file", e);
|
19
|
+
reject(e);
|
14
20
|
}
|
15
|
-
resolve(jsonData);
|
16
21
|
};
|
17
22
|
reader.readAsText(file);
|
18
23
|
} else {
|