@firecms/data_import_export 3.0.0-canary.2 → 3.0.0-canary.200

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 (45) hide show
  1. package/LICENSE +114 -21
  2. package/README.md +1 -4
  3. package/dist/index.d.ts +2 -4
  4. package/dist/index.es.js +15 -971
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +29 -2
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/useImportExportPlugin.d.ts +7 -4
  9. package/package.json +18 -35
  10. package/src/index.ts +2 -4
  11. package/src/useImportExportPlugin.tsx +13 -8
  12. package/dist/components/DataNewPropertiesMapping.d.ts +0 -15
  13. package/dist/components/ImportFileUpload.d.ts +0 -3
  14. package/dist/components/ImportNewPropertyFieldPreview.d.ts +0 -10
  15. package/dist/components/ImportSaveInProgress.d.ts +0 -7
  16. package/dist/components/index.d.ts +0 -4
  17. package/dist/export_import/ExportCollectionAction.d.ts +0 -10
  18. package/dist/export_import/ImportCollectionAction.d.ts +0 -13
  19. package/dist/export_import/export.d.ts +0 -10
  20. package/dist/hooks/index.d.ts +0 -1
  21. package/dist/hooks/useImportConfig.d.ts +0 -2
  22. package/dist/types/column_mapping.d.ts +0 -22
  23. package/dist/types/index.d.ts +0 -1
  24. package/dist/utils/data.d.ts +0 -11
  25. package/dist/utils/file_to_json.d.ts +0 -11
  26. package/dist/utils/get_import_inference_type.d.ts +0 -2
  27. package/dist/utils/get_properties_mapping.d.ts +0 -3
  28. package/dist/utils/index.d.ts +0 -4
  29. package/src/components/DataNewPropertiesMapping.tsx +0 -127
  30. package/src/components/ImportFileUpload.tsx +0 -34
  31. package/src/components/ImportNewPropertyFieldPreview.tsx +0 -55
  32. package/src/components/ImportSaveInProgress.tsx +0 -95
  33. package/src/components/index.ts +0 -4
  34. package/src/export_import/ExportCollectionAction.tsx +0 -251
  35. package/src/export_import/ImportCollectionAction.tsx +0 -420
  36. package/src/export_import/export.ts +0 -200
  37. package/src/hooks/index.ts +0 -1
  38. package/src/hooks/useImportConfig.tsx +0 -28
  39. package/src/types/column_mapping.ts +0 -32
  40. package/src/types/index.ts +0 -1
  41. package/src/utils/data.ts +0 -210
  42. package/src/utils/file_to_json.ts +0 -88
  43. package/src/utils/get_import_inference_type.ts +0 -27
  44. package/src/utils/get_properties_mapping.ts +0 -59
  45. package/src/utils/index.ts +0 -4
@@ -1,127 +0,0 @@
1
- import { getPropertyInPath, Property, } from "@firecms/core";
2
- import {
3
- ChevronRightIcon,
4
- Select,
5
- SelectItem,
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHeader,
10
- TableRow,
11
- Typography
12
- } from "@firecms/ui";
13
-
14
- export interface DataPropertyMappingProps {
15
- idColumn?: string;
16
- headersMapping: Record<string, string | null>;
17
- originProperties: Record<string, Property>;
18
- destinationProperties: Record<string, Property>;
19
- onIdPropertyChanged: (value: string) => void;
20
- buildPropertyView?: (props: {
21
- isIdColumn: boolean,
22
- property: Property | null,
23
- propertyKey: string | null,
24
- importKey: string
25
- }) => React.ReactNode;
26
- }
27
-
28
- export function DataNewPropertiesMapping({
29
- idColumn,
30
- headersMapping,
31
- originProperties,
32
- destinationProperties,
33
- onIdPropertyChanged,
34
- buildPropertyView,
35
- }: DataPropertyMappingProps) {
36
-
37
- return (
38
- <>
39
-
40
- <IdSelectField idColumn={idColumn}
41
- headersMapping={headersMapping}
42
- onChange={onIdPropertyChanged}/>
43
-
44
- <Table style={{
45
- tableLayout: "fixed"
46
- }}>
47
- <TableHeader>
48
- <TableCell header={true} style={{ width: "20%" }}>
49
- Column in file
50
- </TableCell>
51
- <TableCell header={true}>
52
- </TableCell>
53
- <TableCell header={true} style={{ width: "75%" }}>
54
- Property
55
- </TableCell>
56
- </TableHeader>
57
- <TableBody>
58
- {destinationProperties &&
59
- Object.entries(headersMapping)
60
- .map(([importKey, mappedKey]) => {
61
- const propertyKey = headersMapping[importKey];
62
- const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) as Property : null;
63
-
64
- const originProperty = getPropertyInPath(originProperties, importKey) as Property | undefined;
65
- const originDataType = originProperty ? (originProperty.dataType === "array" && typeof originProperty.of === "object"
66
- ? `${originProperty.dataType} - ${(originProperty.of as Property).dataType}`
67
- : originProperty.dataType)
68
- : undefined;
69
- return <TableRow key={importKey} style={{ height: "90px" }}>
70
- <TableCell style={{ width: "20%" }}>
71
- <Typography variant={"body2"}>{importKey}</Typography>
72
- {originProperty && <Typography
73
- variant={"caption"}
74
- color={"secondary"}
75
- >{originDataType}</Typography>}
76
- </TableCell>
77
- <TableCell>
78
- <ChevronRightIcon/>
79
- </TableCell>
80
- <TableCell className={importKey === idColumn ? "text-center" : undefined}
81
- style={{ width: "75%" }}>
82
- {buildPropertyView?.({
83
- isIdColumn: importKey === idColumn,
84
- property,
85
- propertyKey,
86
- importKey
87
- })
88
- }
89
- </TableCell>
90
- </TableRow>;
91
- }
92
- )}
93
- </TableBody>
94
- </Table>
95
- </>
96
- );
97
- }
98
-
99
- function IdSelectField({
100
- idColumn,
101
- headersMapping,
102
- onChange
103
- }: {
104
- idColumn?: string,
105
- headersMapping: Record<string, string | null>;
106
- onChange: (value: string) => void
107
- }) {
108
- return <div>
109
- <Select
110
- size={"small"}
111
- value={idColumn ?? ""}
112
- onChange={(event) => {
113
- onChange(event.target.value as string);
114
- }}
115
- renderValue={(value) => {
116
- return <Typography variant={"body2"}>
117
- {value !== "" ? value : "Autogenerate ID"}
118
- </Typography>;
119
- }}
120
- label={"Column that will be used as ID for each document"}>
121
- <SelectItem value={""}>Autogenerate ID</SelectItem>
122
- {Object.entries(headersMapping).map(([key, value]) => {
123
- return <SelectItem key={key} value={key}>{key}</SelectItem>;
124
- })}
125
- </Select>
126
- </div>;
127
- }
@@ -1,34 +0,0 @@
1
- import { FileUpload, UploadIcon } from "@firecms/ui";
2
- import { convertFileToJson } from "../utils/file_to_json";
3
- import { useSnackbarController } from "@firecms/core";
4
-
5
- export function ImportFileUpload({ onDataAdded }: { onDataAdded: (data: object[]) => void }) {
6
- const snackbarController = useSnackbarController();
7
- return <FileUpload
8
- accept={{
9
- "text/*": [".csv", ".xls", ".xlsx"],
10
- "application/vnd.ms-excel": [".xls", ".xlsx"],
11
- "application/msexcel": [".xls", ".xlsx"],
12
- "application/vnd.ms-office": [".xls", ".xlsx"],
13
- "application/xls": [".xls", ".xlsx"],
14
- "application/x-xls": [".xls", ".xlsx"],
15
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xls", ".xlsx"],
16
- "application/json": [".json"],
17
- }}
18
- preventDropOnDocument={true}
19
- size={"small"}
20
- maxFiles={1}
21
- uploadDescription={<><UploadIcon/>Drag and drop a file here or click to upload</>}
22
- onFilesAdded={(files: File[]) => {
23
- if (files.length > 0) {
24
- convertFileToJson(files[0])
25
- .then((jsonData) => {
26
- onDataAdded(jsonData);
27
- })
28
- .catch((error) => {
29
- console.error("Error parsing file", error);
30
- snackbarController.open({ type: "error", message: error.message });
31
- });
32
- }
33
- }}/>
34
- }
@@ -1,55 +0,0 @@
1
- import React from "react";
2
- import { ErrorBoundary, PropertyConfigBadge, getFieldConfig, Property, useCustomizationController } from "@firecms/core";
3
- import { EditIcon, IconButton, TextField, } from "@firecms/ui";
4
-
5
- export function ImportNewPropertyFieldPreview({
6
- propertyKey,
7
- property,
8
- onEditClick,
9
- includeName = true,
10
- onPropertyNameChanged,
11
- propertyTypeView
12
- }: {
13
- propertyKey: string | null,
14
- property: Property | null
15
- includeName?: boolean,
16
- onEditClick?: () => void,
17
- onPropertyNameChanged?: (propertyKey: string, value: string) => void,
18
- propertyTypeView?: React.ReactNode
19
- }) {
20
-
21
- const { propertyConfigs } = useCustomizationController();
22
- const widget = property ? getFieldConfig(property, propertyConfigs) : null;
23
-
24
- return <ErrorBoundary>
25
- <div
26
- className="flex flex-row w-full items-center">
27
-
28
- <div className={"mx-4"}>
29
- {propertyTypeView ?? <PropertyConfigBadge propertyConfig={widget ?? undefined}/>}
30
- </div>
31
-
32
- <div className="w-full flex flex-col grow">
33
-
34
- <div className={"flex flex-row items-center gap-2"}>
35
- {includeName &&
36
- <TextField
37
- size={"small"}
38
- className={"text-base grow"}
39
- value={property?.name ?? ""}
40
- onChange={(e) => {
41
- if (onPropertyNameChanged && propertyKey)
42
- onPropertyNameChanged(propertyKey, e.target.value);
43
- }}/>}
44
-
45
- <IconButton onClick={onEditClick} size={"small"}>
46
- <EditIcon size={"small"}/>
47
- </IconButton>
48
- </div>
49
-
50
- </div>
51
-
52
-
53
- </div>
54
- </ErrorBoundary>
55
- }
@@ -1,95 +0,0 @@
1
- import { CMSType, DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
2
- import { CenteredView, CircularProgress, Typography, } from "@firecms/ui";
3
- import { useEffect, useRef, useState } from "react";
4
- import { ImportConfig } from "../types";
5
-
6
- export function ImportSaveInProgress<C extends EntityCollection>
7
- ({
8
- importConfig,
9
- collection,
10
- onImportSuccess
11
- }:
12
- {
13
- importConfig: ImportConfig,
14
- collection: C,
15
- onImportSuccess: (collection: C) => void
16
- }) {
17
-
18
- const dataSource = useDataSource();
19
-
20
- const savingRef = useRef<boolean>(false);
21
-
22
- const [processedEntities, setProcessedEntities] = useState<number>(0);
23
-
24
- function save() {
25
-
26
- if (savingRef.current)
27
- return;
28
-
29
- savingRef.current = true;
30
-
31
- saveDataBatch(
32
- dataSource,
33
- collection,
34
- importConfig.entities,
35
- 0,
36
- 25,
37
- setProcessedEntities
38
- ).then(() => {
39
- onImportSuccess(collection);
40
- savingRef.current = false;
41
- });
42
- }
43
-
44
- useEffect(() => {
45
- save();
46
- }, []);
47
-
48
- return (
49
- <CenteredView className={"flex flex-col gap-4 items-center"}>
50
- <CircularProgress/>
51
-
52
- <Typography variant={"h6"}>
53
- Saving data
54
- </Typography>
55
-
56
- <Typography variant={"body2"}>
57
- {processedEntities}/{importConfig.entities.length} entities saved
58
- </Typography>
59
-
60
- <Typography variant={"caption"}>
61
- Do not close this tab or the import will be interrupted.
62
- </Typography>
63
-
64
- </CenteredView>
65
- );
66
-
67
- }
68
-
69
- function saveDataBatch(dataSource: DataSource,
70
- collection: EntityCollection,
71
- data: Partial<Entity<any>>[],
72
- offset = 0,
73
- batchSize = 25,
74
- onProgressUpdate: (progress: number) => void): Promise<void> {
75
-
76
- console.debug("Saving imported data", offset, batchSize);
77
-
78
- const batch = data.slice(offset, offset + batchSize);
79
- return Promise.all(batch.map(d =>
80
- dataSource.saveEntity({
81
- path: collection.path, // TODO: should check if this is correct, specially for subcollections
82
- values: d.values,
83
- entityId: d.id,
84
- collection,
85
- status: "new"
86
- })))
87
- .then(() => {
88
- if (offset + batchSize < data.length) {
89
- onProgressUpdate(offset + batchSize);
90
- return saveDataBatch(dataSource, collection, data, offset + batchSize, batchSize, onProgressUpdate);
91
- }
92
- onProgressUpdate(data.length);
93
- return Promise.resolve();
94
- });
95
- }
@@ -1,4 +0,0 @@
1
- export * from "./DataNewPropertiesMapping";
2
- export * from "./ImportFileUpload";
3
- export * from "./ImportNewPropertyFieldPreview";
4
- export * from "./ImportSaveInProgress";
@@ -1,251 +0,0 @@
1
- import React, { useCallback } from "react";
2
-
3
- import {
4
- CollectionActionsProps,
5
- Entity,
6
- EntityCollection,
7
- ExportConfig,
8
- resolveCollection,
9
- ResolvedEntityCollection,
10
- useCustomizationController,
11
- useDataSource,
12
- useFireCMSContext,
13
- useNavigationController,
14
- User
15
- } from "@firecms/core";
16
- import {
17
- Alert,
18
- BooleanSwitchWithLabel,
19
- Button,
20
- CircularProgress,
21
- cn,
22
- Dialog,
23
- DialogActions,
24
- DialogContent,
25
- focusedMixin,
26
- GetAppIcon,
27
- IconButton,
28
- Tooltip,
29
- Typography,
30
- } from "@firecms/ui";
31
- import { downloadExport } from "./export";
32
-
33
- const DOCS_LIMIT = 500;
34
-
35
- export function ExportCollectionAction<M extends Record<string, any>, UserType extends User>({
36
- collection: inputCollection,
37
- path: inputPath,
38
- collectionEntitiesCount,
39
- exportAllowed,
40
- notAllowedView
41
- }: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
42
- exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: EntityCollection }) => boolean;
43
- notAllowedView?: React.ReactNode;
44
- }) {
45
-
46
- const customizationController = useCustomizationController();
47
-
48
- const exportConfig = typeof inputCollection.exportable === "object" ? inputCollection.exportable : undefined;
49
-
50
- const dateRef = React.useRef<Date>(new Date());
51
- const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);
52
- const [exportType, setExportType] = React.useState<"csv" | "json">("csv");
53
- const [dateExportType, setDateExportType] = React.useState<"timestamp" | "string">("string");
54
-
55
- const context = useFireCMSContext<UserType>();
56
- const dataSource = useDataSource();
57
- const navigationController = useNavigationController();
58
-
59
- const path = navigationController.resolveAliasesFrom(inputPath);
60
-
61
- const canExport = !exportAllowed || exportAllowed({
62
- collectionEntitiesCount,
63
- path,
64
- collection: inputCollection
65
- });
66
-
67
- const collection: ResolvedEntityCollection<M> = React.useMemo(() => resolveCollection({
68
- collection: inputCollection,
69
- path,
70
- fields: customizationController.propertyConfigs
71
- }), [inputCollection, path]);
72
-
73
- const [dataLoading, setDataLoading] = React.useState<boolean>(false);
74
- const [dataLoadingError, setDataLoadingError] = React.useState<Error | undefined>();
75
-
76
- const [open, setOpen] = React.useState(false);
77
-
78
- const handleClickOpen = useCallback(() => {
79
- setOpen(true);
80
- }, [setOpen]);
81
-
82
- const handleClose = useCallback(() => {
83
- setOpen(false);
84
- }, [setOpen]);
85
-
86
- const fetchAdditionalFields = useCallback(async (entities: Entity<M>[]) => {
87
-
88
- const additionalExportFields = exportConfig?.additionalFields;
89
- const additionalFields = collection.additionalFields;
90
-
91
- const resolvedExportColumnsValues: Record<string, any>[] = additionalExportFields
92
- ? await Promise.all(entities.map(async (entity) => {
93
- return (await Promise.all(additionalExportFields.map(async (column) => {
94
- return {
95
- [column.key]: await column.builder({
96
- entity,
97
- context
98
- })
99
- };
100
- }))).reduce((a, b) => ({ ...a, ...b }), {});
101
- }))
102
- : [];
103
-
104
- const resolvedColumnsValues: Record<string, any>[] = additionalFields
105
- ? await Promise.all(entities.map(async (entity) => {
106
- return (await Promise.all(additionalFields
107
- .map(async (field) => {
108
- if (!field.value)
109
- return {};
110
- return {
111
- [field.key]: await field.value({
112
- entity,
113
- context
114
- })
115
- };
116
- }))).reduce((a, b) => ({ ...a, ...b }), {});
117
- }))
118
- : [];
119
- return [...resolvedExportColumnsValues, ...resolvedColumnsValues];
120
- }, [exportConfig?.additionalFields]);
121
-
122
- const doDownload = useCallback(async (collection: ResolvedEntityCollection<M>,
123
- exportConfig: ExportConfig<any> | undefined) => {
124
-
125
- setDataLoading(true);
126
- dataSource.fetchCollection<M>({
127
- path,
128
- collection
129
- })
130
- .then(async (data) => {
131
- setDataLoadingError(undefined);
132
- const additionalData = await fetchAdditionalFields(data);
133
- const additionalHeaders = [
134
- ...exportConfig?.additionalFields?.map(column => column.key) ?? [],
135
- ...collection.additionalFields?.map(field => field.key) ?? []
136
- ];
137
- downloadExport(data, additionalData, collection, flattenArrays, additionalHeaders, exportType, dateExportType);
138
- })
139
- .catch((e) => {
140
- console.error("Error loading export data", e);
141
- setDataLoadingError(e);
142
- })
143
- .finally(() => setDataLoading(false));
144
-
145
- }, [dataSource, path, fetchAdditionalFields, flattenArrays, exportType, dateExportType]);
146
-
147
- const onOkClicked = useCallback(() => {
148
- doDownload(collection, exportConfig);
149
- handleClose();
150
- }, [doDownload, collection, exportConfig, handleClose]);
151
-
152
- return <>
153
-
154
- <Tooltip title={"Export"}>
155
- <IconButton color={"primary"} onClick={handleClickOpen}>
156
- <GetAppIcon/>
157
- </IconButton>
158
- </Tooltip>
159
-
160
- <Dialog
161
- open={open}
162
- onOpenChange={setOpen}
163
- maxWidth={"xl"}>
164
- <DialogContent className={"flex flex-col gap-4 my-4"}>
165
-
166
- <Typography variant={"h6"}>Export data</Typography>
167
-
168
- <div>Download the the content of this table as a CSV</div>
169
-
170
- {collectionEntitiesCount > DOCS_LIMIT &&
171
- <Alert color={"warning"}>
172
- <div>
173
- This collections has a large number
174
- of documents ({collectionEntitiesCount}).
175
- </div>
176
- </Alert>}
177
-
178
- <div className={"flex flex-row gap-4"}>
179
- <div className={"p-4 flex flex-col"}>
180
- <div className="flex items-center">
181
- <input id="radio-csv" type="radio" value="csv" name="exportType"
182
- checked={exportType === "csv"}
183
- onChange={() => setExportType("csv")}
184
- className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
185
- <label htmlFor="radio-csv"
186
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">CSV</label>
187
- </div>
188
- <div className="flex items-center">
189
- <input id="radio-json" type="radio" value="json" name="exportType"
190
- checked={exportType === "json"}
191
- onChange={() => setExportType("json")}
192
- className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
193
- <label htmlFor="radio-json"
194
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">JSON</label>
195
- </div>
196
- </div>
197
-
198
- <div className={"p-4 flex flex-col"}>
199
- <div className="flex items-center">
200
- <input id="radio-timestamp" type="radio" value="timestamp" name="dateExportType"
201
- checked={dateExportType === "timestamp"}
202
- onChange={() => setDateExportType("timestamp")}
203
- className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
204
- <label htmlFor="radio-timestamp"
205
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
206
- timestamps ({dateRef.current.getTime()})</label>
207
- </div>
208
- <div className="flex items-center">
209
- <input id="radio-string" type="radio" value="string" name="dateExportType"
210
- checked={dateExportType === "string"}
211
- onChange={() => setDateExportType("string")}
212
- className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
213
- <label htmlFor="radio-string"
214
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
215
- strings ({dateRef.current.toISOString()})</label>
216
- </div>
217
- </div>
218
- </div>
219
-
220
- <BooleanSwitchWithLabel
221
- size={"small"}
222
- disabled={exportType !== "csv"}
223
- value={flattenArrays}
224
- onValueChange={setFlattenArrays}
225
- label={"Flatten arrays"}/>
226
-
227
- {!canExport && notAllowedView}
228
-
229
- </DialogContent>
230
-
231
- <DialogActions>
232
-
233
- {dataLoading && <CircularProgress size={"small"}/>}
234
-
235
- <Button onClick={handleClose}
236
- variant={"text"}>
237
- Cancel
238
- </Button>
239
-
240
- <Button variant="filled"
241
- onClick={onOkClicked}
242
- disabled={dataLoading || !canExport}>
243
- Download
244
- </Button>
245
-
246
- </DialogActions>
247
-
248
- </Dialog>
249
-
250
- </>;
251
- }