@firecms/data_import_export 3.0.0-alpha.38
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 +21 -0
- package/README.md +87 -0
- package/dist/components/DataNewPropertiesMapping.d.ts +15 -0
- package/dist/components/ImportFileUpload.d.ts +3 -0
- package/dist/components/ImportNewPropertyFieldPreview.d.ts +10 -0
- package/dist/components/ImportSaveInProgress.d.ts +9 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/export_import/ExportCollectionAction.d.ts +10 -0
- package/dist/export_import/ImportCollectionAction.d.ts +13 -0
- package/dist/export_import/export.d.ts +10 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useImportConfig.d.ts +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +966 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +3 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/column_mapping.d.ts +22 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/useImportExportPlugin.d.ts +14 -0
- package/dist/utils/data.d.ts +11 -0
- package/dist/utils/file_to_json.d.ts +11 -0
- package/dist/utils/get_import_inference_type.d.ts +2 -0
- package/dist/utils/get_properties_mapping.d.ts +3 -0
- package/dist/utils/index.d.ts +4 -0
- package/package.json +103 -0
- package/src/components/DataNewPropertiesMapping.tsx +128 -0
- package/src/components/ImportFileUpload.tsx +28 -0
- package/src/components/ImportNewPropertyFieldPreview.tsx +67 -0
- package/src/components/ImportSaveInProgress.tsx +103 -0
- package/src/components/index.ts +4 -0
- package/src/export_import/ExportCollectionAction.tsx +243 -0
- package/src/export_import/ImportCollectionAction.tsx +419 -0
- package/src/export_import/export.ts +198 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useImportConfig.tsx +28 -0
- package/src/index.ts +5 -0
- package/src/types/column_mapping.ts +32 -0
- package/src/types/index.ts +1 -0
- package/src/useImportExportPlugin.tsx +23 -0
- package/src/utils/data.ts +210 -0
- package/src/utils/file_to_json.ts +83 -0
- package/src/utils/get_import_inference_type.ts +27 -0
- package/src/utils/get_properties_mapping.ts +60 -0
- package/src/utils/index.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Alert,
|
|
5
|
+
BooleanSwitchWithLabel,
|
|
6
|
+
Button,
|
|
7
|
+
CircularProgress,
|
|
8
|
+
cn,
|
|
9
|
+
CollectionActionsProps,
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogActions,
|
|
12
|
+
DialogContent,
|
|
13
|
+
Entity,
|
|
14
|
+
EntityCollection,
|
|
15
|
+
ExportConfig,
|
|
16
|
+
focusedMixin,
|
|
17
|
+
GetAppIcon,
|
|
18
|
+
IconButton,
|
|
19
|
+
resolveCollection,
|
|
20
|
+
ResolvedEntityCollection,
|
|
21
|
+
Tooltip,
|
|
22
|
+
Typography,
|
|
23
|
+
useDataSource,
|
|
24
|
+
useFireCMSContext,
|
|
25
|
+
useNavigationContext,
|
|
26
|
+
User
|
|
27
|
+
} from "@firecms/core";
|
|
28
|
+
import { downloadExport } from "./export";
|
|
29
|
+
|
|
30
|
+
const DOCS_LIMIT = 500;
|
|
31
|
+
|
|
32
|
+
export function ExportCollectionAction<M extends Record<string, any>, UserType extends User>({
|
|
33
|
+
collection: inputCollection,
|
|
34
|
+
path: inputPath,
|
|
35
|
+
collectionEntitiesCount,
|
|
36
|
+
exportAllowed,
|
|
37
|
+
notAllowedView
|
|
38
|
+
}: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
|
|
39
|
+
exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: EntityCollection }) => boolean;
|
|
40
|
+
notAllowedView?: React.ReactNode;
|
|
41
|
+
}) {
|
|
42
|
+
|
|
43
|
+
const exportConfig = typeof inputCollection.exportable === "object" ? inputCollection.exportable : undefined;
|
|
44
|
+
|
|
45
|
+
const dateRef = React.useRef<Date>(new Date());
|
|
46
|
+
const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);
|
|
47
|
+
const [exportType, setExportType] = React.useState<"csv" | "json">("csv");
|
|
48
|
+
const [dateExportType, setDateExportType] = React.useState<"timestamp" | "string">("string");
|
|
49
|
+
|
|
50
|
+
const context = useFireCMSContext<UserType>();
|
|
51
|
+
const dataSource = useDataSource();
|
|
52
|
+
const navigationContext = useNavigationContext();
|
|
53
|
+
|
|
54
|
+
const path = navigationContext.resolveAliasesFrom(inputPath);
|
|
55
|
+
|
|
56
|
+
const canExport = !exportAllowed || exportAllowed({
|
|
57
|
+
collectionEntitiesCount,
|
|
58
|
+
path,
|
|
59
|
+
collection: inputCollection
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const collection: ResolvedEntityCollection<M> = React.useMemo(() => resolveCollection({
|
|
63
|
+
collection: inputCollection,
|
|
64
|
+
path,
|
|
65
|
+
fields: context.propertyConfigs
|
|
66
|
+
}), [inputCollection, path]);
|
|
67
|
+
|
|
68
|
+
const [dataLoading, setDataLoading] = React.useState<boolean>(false);
|
|
69
|
+
const [dataLoadingError, setDataLoadingError] = React.useState<Error | undefined>();
|
|
70
|
+
|
|
71
|
+
const [open, setOpen] = React.useState(false);
|
|
72
|
+
|
|
73
|
+
const handleClickOpen = useCallback(() => {
|
|
74
|
+
setOpen(true);
|
|
75
|
+
}, [setOpen]);
|
|
76
|
+
|
|
77
|
+
const handleClose = useCallback(() => {
|
|
78
|
+
setOpen(false);
|
|
79
|
+
}, [setOpen]);
|
|
80
|
+
|
|
81
|
+
const fetchAdditionalFields = useCallback(async (entities: Entity<M>[]) => {
|
|
82
|
+
|
|
83
|
+
const additionalExportFields = exportConfig?.additionalFields;
|
|
84
|
+
const additionalFields = collection.additionalFields;
|
|
85
|
+
|
|
86
|
+
const resolvedExportColumnsValues: Record<string, any>[] = additionalExportFields
|
|
87
|
+
? await Promise.all(entities.map(async (entity) => {
|
|
88
|
+
return (await Promise.all(additionalExportFields.map(async (column) => {
|
|
89
|
+
return {
|
|
90
|
+
[column.key]: await column.builder({
|
|
91
|
+
entity,
|
|
92
|
+
context
|
|
93
|
+
})
|
|
94
|
+
};
|
|
95
|
+
}))).reduce((a, b) => ({ ...a, ...b }), {});
|
|
96
|
+
}))
|
|
97
|
+
: [];
|
|
98
|
+
|
|
99
|
+
const resolvedColumnsValues: Record<string, any>[] = additionalFields
|
|
100
|
+
? await Promise.all(entities.map(async (entity) => {
|
|
101
|
+
return (await Promise.all(additionalFields
|
|
102
|
+
.map(async (field) => {
|
|
103
|
+
if (!field.value)
|
|
104
|
+
return {};
|
|
105
|
+
return {
|
|
106
|
+
[field.key]: await field.value({
|
|
107
|
+
entity,
|
|
108
|
+
context
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
}))).reduce((a, b) => ({ ...a, ...b }), {});
|
|
112
|
+
}))
|
|
113
|
+
: [];
|
|
114
|
+
return [...resolvedExportColumnsValues, ...resolvedColumnsValues];
|
|
115
|
+
}, [exportConfig?.additionalFields]);
|
|
116
|
+
|
|
117
|
+
const doDownload = useCallback(async (collection: ResolvedEntityCollection<M>,
|
|
118
|
+
exportConfig: ExportConfig<any> | undefined) => {
|
|
119
|
+
|
|
120
|
+
setDataLoading(true);
|
|
121
|
+
dataSource.fetchCollection<M>({
|
|
122
|
+
path,
|
|
123
|
+
collection
|
|
124
|
+
})
|
|
125
|
+
.then(async (data) => {
|
|
126
|
+
setDataLoadingError(undefined);
|
|
127
|
+
const additionalData = await fetchAdditionalFields(data);
|
|
128
|
+
const additionalHeaders = [
|
|
129
|
+
...exportConfig?.additionalFields?.map(column => column.key) ?? [],
|
|
130
|
+
...collection.additionalFields?.map(field => field.key) ?? []
|
|
131
|
+
];
|
|
132
|
+
downloadExport(data, additionalData, collection, flattenArrays, additionalHeaders, exportType, dateExportType);
|
|
133
|
+
})
|
|
134
|
+
.catch(setDataLoadingError)
|
|
135
|
+
.finally(() => setDataLoading(false));
|
|
136
|
+
|
|
137
|
+
}, [dataSource, path, fetchAdditionalFields, flattenArrays, exportType, dateExportType]);
|
|
138
|
+
|
|
139
|
+
const onOkClicked = useCallback(() => {
|
|
140
|
+
doDownload(collection, exportConfig);
|
|
141
|
+
handleClose();
|
|
142
|
+
}, [doDownload, collection, exportConfig, handleClose]);
|
|
143
|
+
|
|
144
|
+
return <>
|
|
145
|
+
|
|
146
|
+
<Tooltip title={"Export"}>
|
|
147
|
+
<IconButton color={"primary"} onClick={handleClickOpen}>
|
|
148
|
+
<GetAppIcon/>
|
|
149
|
+
</IconButton>
|
|
150
|
+
</Tooltip>
|
|
151
|
+
|
|
152
|
+
<Dialog
|
|
153
|
+
open={open}
|
|
154
|
+
onOpenChange={setOpen}
|
|
155
|
+
maxWidth={"xl"}>
|
|
156
|
+
<DialogContent className={"flex flex-col gap-4 my-4"}>
|
|
157
|
+
|
|
158
|
+
<Typography variant={"h6"}>Export data</Typography>
|
|
159
|
+
|
|
160
|
+
<div>Download the the content of this table as a CSV</div>
|
|
161
|
+
|
|
162
|
+
{collectionEntitiesCount > DOCS_LIMIT &&
|
|
163
|
+
<Alert color={"warning"}>
|
|
164
|
+
<div>
|
|
165
|
+
This collections has a large number
|
|
166
|
+
of documents ({collectionEntitiesCount}).
|
|
167
|
+
</div>
|
|
168
|
+
</Alert>}
|
|
169
|
+
|
|
170
|
+
<div className={"flex flex-row gap-4"}>
|
|
171
|
+
<div className={"p-4 flex flex-col"}>
|
|
172
|
+
<div className="flex items-center">
|
|
173
|
+
<input id="radio-csv" type="radio" value="csv" name="exportType"
|
|
174
|
+
checked={exportType === "csv"}
|
|
175
|
+
onChange={() => setExportType("csv")}
|
|
176
|
+
className={cn(focusedMixin, "w-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
|
177
|
+
<label htmlFor="radio-csv"
|
|
178
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">CSV</label>
|
|
179
|
+
</div>
|
|
180
|
+
<div className="flex items-center">
|
|
181
|
+
<input id="radio-json" type="radio" value="json" name="exportType"
|
|
182
|
+
checked={exportType === "json"}
|
|
183
|
+
onChange={() => setExportType("json")}
|
|
184
|
+
className={cn(focusedMixin, "w-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
|
185
|
+
<label htmlFor="radio-json"
|
|
186
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">JSON</label>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className={"p-4 flex flex-col"}>
|
|
191
|
+
<div className="flex items-center">
|
|
192
|
+
<input id="radio-timestamp" type="radio" value="timestamp" name="dateExportType"
|
|
193
|
+
checked={dateExportType === "timestamp"}
|
|
194
|
+
onChange={() => setDateExportType("timestamp")}
|
|
195
|
+
className={cn(focusedMixin, "w-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
|
196
|
+
<label htmlFor="radio-timestamp"
|
|
197
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
|
|
198
|
+
timestamps ({dateRef.current.getTime()})</label>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex items-center">
|
|
201
|
+
<input id="radio-string" type="radio" value="string" name="dateExportType"
|
|
202
|
+
checked={dateExportType === "string"}
|
|
203
|
+
onChange={() => setDateExportType("string")}
|
|
204
|
+
className={cn(focusedMixin, "w-4 text-blue-600 bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
|
205
|
+
<label htmlFor="radio-string"
|
|
206
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
|
|
207
|
+
strings ({dateRef.current.toISOString()})</label>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<BooleanSwitchWithLabel
|
|
213
|
+
size={"small"}
|
|
214
|
+
disabled={exportType !== "csv"}
|
|
215
|
+
value={flattenArrays}
|
|
216
|
+
onValueChange={setFlattenArrays}
|
|
217
|
+
label={"Flatten arrays"}/>
|
|
218
|
+
|
|
219
|
+
{!canExport && notAllowedView}
|
|
220
|
+
|
|
221
|
+
</DialogContent>
|
|
222
|
+
|
|
223
|
+
<DialogActions>
|
|
224
|
+
|
|
225
|
+
{dataLoading && <CircularProgress size={"small"}/>}
|
|
226
|
+
|
|
227
|
+
<Button onClick={handleClose}
|
|
228
|
+
variant={"text"}>
|
|
229
|
+
Cancel
|
|
230
|
+
</Button>
|
|
231
|
+
|
|
232
|
+
<Button variant="filled"
|
|
233
|
+
onClick={onOkClicked}
|
|
234
|
+
disabled={dataLoading || !canExport}>
|
|
235
|
+
Download
|
|
236
|
+
</Button>
|
|
237
|
+
|
|
238
|
+
</DialogActions>
|
|
239
|
+
|
|
240
|
+
</Dialog>
|
|
241
|
+
|
|
242
|
+
</>;
|
|
243
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
cn,
|
|
5
|
+
CollectionActionsProps,
|
|
6
|
+
defaultBorderMixin,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogActions,
|
|
9
|
+
DialogContent,
|
|
10
|
+
EntityCollectionTable,
|
|
11
|
+
FieldConfigBadge,
|
|
12
|
+
FileUploadIcon,
|
|
13
|
+
getFieldConfig,
|
|
14
|
+
getPropertiesWithPropertiesOrder,
|
|
15
|
+
getPropertyInPath,
|
|
16
|
+
IconButton,
|
|
17
|
+
Properties,
|
|
18
|
+
Property,
|
|
19
|
+
resolveCollection,
|
|
20
|
+
ResolvedProperties,
|
|
21
|
+
Select,
|
|
22
|
+
SelectItem,
|
|
23
|
+
Tooltip,
|
|
24
|
+
Typography,
|
|
25
|
+
useFireCMSContext,
|
|
26
|
+
User,
|
|
27
|
+
useSelectionController,
|
|
28
|
+
useSnackbarController
|
|
29
|
+
} from "@firecms/core";
|
|
30
|
+
import { buildEntityPropertiesFromData } from "@firecms/schema_inference";
|
|
31
|
+
import { useImportConfig } from "../hooks";
|
|
32
|
+
import { convertDataToEntity, getInferenceType, getPropertiesMapping } from "../utils";
|
|
33
|
+
import { DataNewPropertiesMapping, ImportFileUpload, ImportSaveInProgress } from "../components";
|
|
34
|
+
import { ImportConfig } from "../types";
|
|
35
|
+
|
|
36
|
+
type ImportState = "initial" | "mapping" | "preview" | "import_data_saving";
|
|
37
|
+
|
|
38
|
+
export function ImportCollectionAction<M extends Record<string, any>, UserType extends User>({
|
|
39
|
+
collection,
|
|
40
|
+
path,
|
|
41
|
+
collectionEntitiesCount,
|
|
42
|
+
}: CollectionActionsProps<M, UserType>
|
|
43
|
+
) {
|
|
44
|
+
const context = useFireCMSContext();
|
|
45
|
+
|
|
46
|
+
const snackbarController = useSnackbarController();
|
|
47
|
+
|
|
48
|
+
const [open, setOpen] = React.useState(false);
|
|
49
|
+
|
|
50
|
+
const [step, setStep] = React.useState<ImportState>("initial");
|
|
51
|
+
|
|
52
|
+
const importConfig = useImportConfig();
|
|
53
|
+
|
|
54
|
+
const handleClickOpen = useCallback(() => {
|
|
55
|
+
setOpen(true);
|
|
56
|
+
setStep("initial");
|
|
57
|
+
}, [setOpen]);
|
|
58
|
+
|
|
59
|
+
const handleClose = useCallback(() => {
|
|
60
|
+
setOpen(false);
|
|
61
|
+
}, [setOpen]);
|
|
62
|
+
|
|
63
|
+
const onMappingComplete = useCallback(() => {
|
|
64
|
+
setStep("preview");
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const onPreviewComplete = useCallback(() => {
|
|
68
|
+
setStep("import_data_saving");
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const onDataAdded = async (data: object[]) => {
|
|
72
|
+
importConfig.setImportData(data);
|
|
73
|
+
|
|
74
|
+
if (data.length > 0) {
|
|
75
|
+
const originProperties = await buildEntityPropertiesFromData(data, getInferenceType);
|
|
76
|
+
importConfig.setOriginProperties(originProperties);
|
|
77
|
+
|
|
78
|
+
const headersMapping = buildHeadersMappingFromData(data);
|
|
79
|
+
importConfig.setHeadersMapping(headersMapping);
|
|
80
|
+
const firstKey = Object.keys(headersMapping)?.[0];
|
|
81
|
+
if (firstKey?.includes("id") || firstKey?.includes("key")) {
|
|
82
|
+
const idColumn = firstKey;
|
|
83
|
+
importConfig.setIdColumn(idColumn);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
setStep("mapping");
|
|
88
|
+
}, 100);
|
|
89
|
+
// setStep("mapping");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const resolvedCollection = resolveCollection({
|
|
93
|
+
collection,
|
|
94
|
+
path,
|
|
95
|
+
fields: context.propertyConfigs
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const properties = getPropertiesWithPropertiesOrder<M>(resolvedCollection.properties, resolvedCollection.propertiesOrder as Extract<keyof M, string>[]) as ResolvedProperties<M>;
|
|
99
|
+
|
|
100
|
+
const propertiesAndLevel = Object.entries(properties)
|
|
101
|
+
.flatMap(([key, property]) => getPropertiesAndLevel(key, property, 0));
|
|
102
|
+
const propertiesOrder = (resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties)) as Extract<keyof M, string>[];
|
|
103
|
+
if (collection.collectionGroup) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return <>
|
|
107
|
+
|
|
108
|
+
<Tooltip title={"Import"}>
|
|
109
|
+
<IconButton color={"primary"} onClick={handleClickOpen}>
|
|
110
|
+
<FileUploadIcon/>
|
|
111
|
+
</IconButton>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
|
|
114
|
+
<Dialog open={open}
|
|
115
|
+
fullWidth={step === "preview"}
|
|
116
|
+
fullHeight={step === "preview"}
|
|
117
|
+
maxWidth={step === "initial" ? "lg" : "7xl"}>
|
|
118
|
+
<DialogContent className={"flex flex-col gap-4 my-4"} fullHeight={step === "preview"}>
|
|
119
|
+
|
|
120
|
+
{step === "initial" && <>
|
|
121
|
+
<Typography variant={"h6"}>Import data</Typography>
|
|
122
|
+
<Typography variant={"body2"}>Upload a CSV, Excel or JSON file and map it to your existing
|
|
123
|
+
schema</Typography>
|
|
124
|
+
<ImportFileUpload onDataAdded={onDataAdded}/>
|
|
125
|
+
</>}
|
|
126
|
+
|
|
127
|
+
{step === "mapping" && <>
|
|
128
|
+
<Typography variant={"h6"}>Map fields</Typography>
|
|
129
|
+
<DataNewPropertiesMapping headersMapping={importConfig.headersMapping}
|
|
130
|
+
idColumn={importConfig.idColumn}
|
|
131
|
+
originProperties={importConfig.originProperties}
|
|
132
|
+
destinationProperties={properties}
|
|
133
|
+
onIdPropertyChanged={(value) => importConfig.setIdColumn(value)}
|
|
134
|
+
buildPropertyView={({
|
|
135
|
+
isIdColumn,
|
|
136
|
+
property,
|
|
137
|
+
propertyKey,
|
|
138
|
+
importKey
|
|
139
|
+
}) => {
|
|
140
|
+
return <PropertyTreeSelect
|
|
141
|
+
selectedPropertyKey={propertyKey ?? ""}
|
|
142
|
+
properties={properties}
|
|
143
|
+
propertiesAndLevel={propertiesAndLevel}
|
|
144
|
+
isIdColumn={isIdColumn}
|
|
145
|
+
onIdSelected={() => {
|
|
146
|
+
importConfig.setIdColumn(importKey);
|
|
147
|
+
}}
|
|
148
|
+
onPropertySelected={(newPropertyKey) => {
|
|
149
|
+
|
|
150
|
+
const newHeadersMapping: Record<string, string | null> = Object.entries(importConfig.headersMapping)
|
|
151
|
+
.map(([currentImportKey, currentPropertyKey]) => {
|
|
152
|
+
if (currentPropertyKey === newPropertyKey) {
|
|
153
|
+
return { [currentImportKey]: null };
|
|
154
|
+
}
|
|
155
|
+
if (currentImportKey === importKey) {
|
|
156
|
+
return { [currentImportKey]: newPropertyKey };
|
|
157
|
+
}
|
|
158
|
+
return { [currentImportKey]: currentPropertyKey };
|
|
159
|
+
})
|
|
160
|
+
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
161
|
+
importConfig.setHeadersMapping(newHeadersMapping as Record<string, string>);
|
|
162
|
+
|
|
163
|
+
if (newPropertyKey === importConfig.idColumn) {
|
|
164
|
+
importConfig.setIdColumn(undefined);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
}}
|
|
168
|
+
/>;
|
|
169
|
+
}}/>
|
|
170
|
+
</>}
|
|
171
|
+
|
|
172
|
+
{step === "preview" && <ImportDataPreview importConfig={importConfig}
|
|
173
|
+
properties={properties as Properties<M>}
|
|
174
|
+
propertiesOrder={propertiesOrder}/>}
|
|
175
|
+
|
|
176
|
+
{step === "import_data_saving" && importConfig &&
|
|
177
|
+
<ImportSaveInProgress importConfig={importConfig}
|
|
178
|
+
collection={collection}
|
|
179
|
+
onImportSuccess={(importedCollection) => {
|
|
180
|
+
handleClose();
|
|
181
|
+
snackbarController.open({
|
|
182
|
+
type: "info",
|
|
183
|
+
message: "Data imported successfully"
|
|
184
|
+
});
|
|
185
|
+
}}
|
|
186
|
+
/>}
|
|
187
|
+
|
|
188
|
+
</DialogContent>
|
|
189
|
+
<DialogActions>
|
|
190
|
+
|
|
191
|
+
{step === "mapping" && <Button onClick={() => setStep("initial")}
|
|
192
|
+
variant={"text"}>
|
|
193
|
+
Back
|
|
194
|
+
</Button>}
|
|
195
|
+
|
|
196
|
+
{step === "preview" && <Button onClick={() => setStep("mapping")}
|
|
197
|
+
variant={"text"}>
|
|
198
|
+
Back
|
|
199
|
+
</Button>}
|
|
200
|
+
|
|
201
|
+
<Button onClick={handleClose}
|
|
202
|
+
variant={"text"}>
|
|
203
|
+
Cancel
|
|
204
|
+
</Button>
|
|
205
|
+
|
|
206
|
+
{step === "mapping" && <Button variant="filled"
|
|
207
|
+
onClick={onMappingComplete}>
|
|
208
|
+
Next
|
|
209
|
+
</Button>}
|
|
210
|
+
|
|
211
|
+
{step === "preview" && <Button variant="filled"
|
|
212
|
+
onClick={onPreviewComplete}>
|
|
213
|
+
Save data
|
|
214
|
+
</Button>}
|
|
215
|
+
|
|
216
|
+
</DialogActions>
|
|
217
|
+
</Dialog>
|
|
218
|
+
|
|
219
|
+
</>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const internalIDValue = "__internal_id__";
|
|
223
|
+
|
|
224
|
+
function PropertyTreeSelect({
|
|
225
|
+
selectedPropertyKey,
|
|
226
|
+
properties,
|
|
227
|
+
onPropertySelected,
|
|
228
|
+
onIdSelected,
|
|
229
|
+
propertiesAndLevel,
|
|
230
|
+
isIdColumn
|
|
231
|
+
}: {
|
|
232
|
+
selectedPropertyKey: string | null;
|
|
233
|
+
properties: Record<string, Property>;
|
|
234
|
+
onPropertySelected: (propertyKey: string | null) => void;
|
|
235
|
+
onIdSelected: () => void;
|
|
236
|
+
propertiesAndLevel: PropertyAndLevel[];
|
|
237
|
+
isIdColumn?: boolean;
|
|
238
|
+
}) {
|
|
239
|
+
|
|
240
|
+
const selectedProperty = selectedPropertyKey ? getPropertyInPath(properties, selectedPropertyKey) : null;
|
|
241
|
+
|
|
242
|
+
const renderValue = useCallback((selectedPropertyKey: string) => {
|
|
243
|
+
|
|
244
|
+
if (selectedPropertyKey === internalIDValue) {
|
|
245
|
+
return <Typography variant={"body2"} className={"p-4"}>Use this column as ID</Typography>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!selectedPropertyKey || !selectedProperty) {
|
|
249
|
+
return <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return <PropertySelectEntry propertyKey={selectedPropertyKey}
|
|
253
|
+
property={selectedProperty as Property}/>;
|
|
254
|
+
}, [selectedProperty]);
|
|
255
|
+
|
|
256
|
+
const onSelectValueChange = useCallback((value: string) => {
|
|
257
|
+
if (value === internalIDValue) {
|
|
258
|
+
onIdSelected();
|
|
259
|
+
onPropertySelected(null);
|
|
260
|
+
} else if (value === "") {
|
|
261
|
+
onPropertySelected(null);
|
|
262
|
+
} else {
|
|
263
|
+
onPropertySelected(value);
|
|
264
|
+
}
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
267
|
+
return <Select value={isIdColumn ? internalIDValue : (selectedPropertyKey ?? undefined)}
|
|
268
|
+
onValueChange={onSelectValueChange}
|
|
269
|
+
renderValue={renderValue}>
|
|
270
|
+
|
|
271
|
+
<SelectItem value={""}>
|
|
272
|
+
<Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
|
|
273
|
+
</SelectItem>
|
|
274
|
+
|
|
275
|
+
<SelectItem value={internalIDValue}>
|
|
276
|
+
<Typography variant={"body2"} className={"p-4"}>Use this column as ID</Typography>
|
|
277
|
+
</SelectItem>
|
|
278
|
+
|
|
279
|
+
{propertiesAndLevel.map(({
|
|
280
|
+
property,
|
|
281
|
+
level,
|
|
282
|
+
propertyKey
|
|
283
|
+
}) => {
|
|
284
|
+
return <SelectItem value={propertyKey}
|
|
285
|
+
key={propertyKey}
|
|
286
|
+
disabled={property.dataType === "map"}>
|
|
287
|
+
<PropertySelectEntry propertyKey={propertyKey}
|
|
288
|
+
property={property}
|
|
289
|
+
level={level}/>
|
|
290
|
+
</SelectItem>;
|
|
291
|
+
})}
|
|
292
|
+
|
|
293
|
+
</Select>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type PropertyAndLevel = {
|
|
297
|
+
property: Property,
|
|
298
|
+
level: number,
|
|
299
|
+
propertyKey: string
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
function getPropertiesAndLevel(key: string, property: Property, level: number): PropertyAndLevel[] {
|
|
303
|
+
const properties: PropertyAndLevel[] = [];
|
|
304
|
+
properties.push({
|
|
305
|
+
property,
|
|
306
|
+
level,
|
|
307
|
+
propertyKey: key
|
|
308
|
+
});
|
|
309
|
+
if (property.dataType === "map" && property.properties) {
|
|
310
|
+
Object.entries(property.properties).forEach(([childKey, value]) => {
|
|
311
|
+
properties.push(...getPropertiesAndLevel(`${key}.${childKey}`, value as Property, level + 1));
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return properties;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function PropertySelectEntry({
|
|
318
|
+
propertyKey,
|
|
319
|
+
property,
|
|
320
|
+
level = 0
|
|
321
|
+
}: {
|
|
322
|
+
propertyKey: string;
|
|
323
|
+
property: Property;
|
|
324
|
+
level?: number;
|
|
325
|
+
}) {
|
|
326
|
+
|
|
327
|
+
const { propertyConfigs } = useFireCMSContext();
|
|
328
|
+
const widget = getFieldConfig(property, propertyConfigs);
|
|
329
|
+
|
|
330
|
+
return <div
|
|
331
|
+
className="flex flex-row w-full text-start items-center h-full">
|
|
332
|
+
|
|
333
|
+
{new Array(level).fill(0).map((_, index) =>
|
|
334
|
+
<div className={cn(defaultBorderMixin, "ml-8 border-l h-12")} key={index}/>)}
|
|
335
|
+
|
|
336
|
+
<div className={"m-4"}>
|
|
337
|
+
<Tooltip title={widget?.name}>
|
|
338
|
+
<FieldConfigBadge propertyConfig={widget}/>
|
|
339
|
+
</Tooltip>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className={"flex flex-col flex-grow p-2 pl-2"}>
|
|
343
|
+
<Typography variant="body1"
|
|
344
|
+
component="span"
|
|
345
|
+
className="flex-grow pr-2">
|
|
346
|
+
{property.name
|
|
347
|
+
? property.name
|
|
348
|
+
: "\u00a0"
|
|
349
|
+
}
|
|
350
|
+
</Typography>
|
|
351
|
+
|
|
352
|
+
<Typography className=" pr-2"
|
|
353
|
+
variant={"body2"}
|
|
354
|
+
component="span"
|
|
355
|
+
color="secondary">
|
|
356
|
+
{propertyKey}
|
|
357
|
+
</Typography>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
</div>;
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function ImportDataPreview<M extends Record<string, any>>({
|
|
365
|
+
importConfig,
|
|
366
|
+
properties,
|
|
367
|
+
propertiesOrder
|
|
368
|
+
}: {
|
|
369
|
+
importConfig: ImportConfig,
|
|
370
|
+
properties: Properties<M>,
|
|
371
|
+
propertiesOrder: Extract<keyof M, string>[]
|
|
372
|
+
}) {
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
const propertiesMapping = getPropertiesMapping(importConfig.originProperties, properties);
|
|
376
|
+
const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, propertiesMapping, "TEMP_PATH"));
|
|
377
|
+
importConfig.setEntities(mappedData);
|
|
378
|
+
}, []);
|
|
379
|
+
|
|
380
|
+
const selectionController = useSelectionController();
|
|
381
|
+
|
|
382
|
+
return <EntityCollectionTable
|
|
383
|
+
title={<div>
|
|
384
|
+
<Typography variant={"subtitle2"}>Imported data preview</Typography>
|
|
385
|
+
<Typography variant={"caption"}>Entities with the same id will be overwritten</Typography>
|
|
386
|
+
</div>}
|
|
387
|
+
tableController={{
|
|
388
|
+
data: importConfig.entities,
|
|
389
|
+
dataLoading: false,
|
|
390
|
+
noMoreToLoad: false
|
|
391
|
+
}}
|
|
392
|
+
endAdornment={<div className={"h-12"}/>}
|
|
393
|
+
filterable={false}
|
|
394
|
+
sortable={false}
|
|
395
|
+
selectionController={selectionController}
|
|
396
|
+
displayedColumnIds={propertiesOrder.map(p => ({
|
|
397
|
+
key: p,
|
|
398
|
+
disabled: false
|
|
399
|
+
}))}
|
|
400
|
+
properties={properties}/>
|
|
401
|
+
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildHeadersMappingFromData(objArr: object[]) {
|
|
405
|
+
const headersMapping: Record<string, string> = {};
|
|
406
|
+
objArr.forEach((obj) => {
|
|
407
|
+
Object.keys(obj).forEach((key) => {
|
|
408
|
+
// @ts-ignore
|
|
409
|
+
const child = obj[key];
|
|
410
|
+
if (typeof child === "object" && !Array.isArray(child)) {
|
|
411
|
+
Object.entries(buildHeadersMappingFromData([child])).forEach(([subKey, mapping]) => {
|
|
412
|
+
headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
headersMapping[key] = key;
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
return headersMapping;
|
|
419
|
+
}
|