@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/dist/components/DataNewPropertiesMapping.d.ts +15 -0
  4. package/dist/components/ImportFileUpload.d.ts +3 -0
  5. package/dist/components/ImportNewPropertyFieldPreview.d.ts +10 -0
  6. package/dist/components/ImportSaveInProgress.d.ts +9 -0
  7. package/dist/components/index.d.ts +4 -0
  8. package/dist/export_import/ExportCollectionAction.d.ts +10 -0
  9. package/dist/export_import/ImportCollectionAction.d.ts +13 -0
  10. package/dist/export_import/export.d.ts +10 -0
  11. package/dist/hooks/index.d.ts +1 -0
  12. package/dist/hooks/useImportConfig.d.ts +2 -0
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.es.js +966 -0
  15. package/dist/index.es.js.map +1 -0
  16. package/dist/index.umd.js +3 -0
  17. package/dist/index.umd.js.map +1 -0
  18. package/dist/types/column_mapping.d.ts +22 -0
  19. package/dist/types/index.d.ts +1 -0
  20. package/dist/useImportExportPlugin.d.ts +14 -0
  21. package/dist/utils/data.d.ts +11 -0
  22. package/dist/utils/file_to_json.d.ts +11 -0
  23. package/dist/utils/get_import_inference_type.d.ts +2 -0
  24. package/dist/utils/get_properties_mapping.d.ts +3 -0
  25. package/dist/utils/index.d.ts +4 -0
  26. package/package.json +103 -0
  27. package/src/components/DataNewPropertiesMapping.tsx +128 -0
  28. package/src/components/ImportFileUpload.tsx +28 -0
  29. package/src/components/ImportNewPropertyFieldPreview.tsx +67 -0
  30. package/src/components/ImportSaveInProgress.tsx +103 -0
  31. package/src/components/index.ts +4 -0
  32. package/src/export_import/ExportCollectionAction.tsx +243 -0
  33. package/src/export_import/ImportCollectionAction.tsx +419 -0
  34. package/src/export_import/export.ts +198 -0
  35. package/src/hooks/index.ts +1 -0
  36. package/src/hooks/useImportConfig.tsx +28 -0
  37. package/src/index.ts +5 -0
  38. package/src/types/column_mapping.ts +32 -0
  39. package/src/types/index.ts +1 -0
  40. package/src/useImportExportPlugin.tsx +23 -0
  41. package/src/utils/data.ts +210 -0
  42. package/src/utils/file_to_json.ts +83 -0
  43. package/src/utils/get_import_inference_type.ts +27 -0
  44. package/src/utils/get_properties_mapping.ts +60 -0
  45. package/src/utils/index.ts +4 -0
  46. 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
+ }