@firecms/data_import_export 3.0.0-3.0.0-beta.4.pre.1.0

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 +8 -0
  7. package/dist/components/index.d.ts +4 -0
  8. package/dist/export_import/ExportCollectionAction.d.ts +11 -0
  9. package/dist/export_import/ImportCollectionAction.d.ts +15 -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 +1001 -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 +16 -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 +34 -0
  29. package/src/components/ImportNewPropertyFieldPreview.tsx +55 -0
  30. package/src/components/ImportSaveInProgress.tsx +122 -0
  31. package/src/components/index.ts +4 -0
  32. package/src/export_import/ExportCollectionAction.tsx +252 -0
  33. package/src/export_import/ImportCollectionAction.tsx +442 -0
  34. package/src/export_import/export.ts +200 -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 +25 -0
  41. package/src/utils/data.ts +223 -0
  42. package/src/utils/file_to_json.ts +88 -0
  43. package/src/utils/get_import_inference_type.ts +27 -0
  44. package/src/utils/get_properties_mapping.ts +59 -0
  45. package/src/utils/index.ts +4 -0
  46. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { DataType, Entity, Property } from "@firecms/core";
3
+ export type ImportConfig = {
4
+ inUse: boolean;
5
+ setInUse: React.Dispatch<React.SetStateAction<boolean>>;
6
+ idColumn: string | undefined;
7
+ setIdColumn: React.Dispatch<React.SetStateAction<string | undefined>>;
8
+ importData: object[];
9
+ setImportData: React.Dispatch<React.SetStateAction<object[]>>;
10
+ entities: Entity<any>[];
11
+ setEntities: React.Dispatch<React.SetStateAction<Entity<any>[]>>;
12
+ headersMapping: Record<string, string | null>;
13
+ setHeadersMapping: React.Dispatch<React.SetStateAction<Record<string, string | null>>>;
14
+ originProperties: Record<string, Property>;
15
+ setOriginProperties: React.Dispatch<React.SetStateAction<Record<string, Property>>>;
16
+ };
17
+ export type DataTypeMapping = {
18
+ from: DataType;
19
+ fromSubtype?: DataType;
20
+ to: DataType;
21
+ toSubtype?: DataType;
22
+ };
@@ -0,0 +1 @@
1
+ export * from "./column_mapping";
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { EntityCollection, FireCMSPlugin } from "@firecms/core";
3
+ /**
4
+ *
5
+ */
6
+ export declare function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSPlugin<any, any, any, ImportExportPluginProps>;
7
+ export type ImportExportPluginProps = {
8
+ exportAllowed?: (props: ExportAllowedParams) => boolean;
9
+ notAllowedView: React.ReactNode;
10
+ onAnalyticsEvent?: (event: string, params?: any) => void;
11
+ };
12
+ export type ExportAllowedParams = {
13
+ collectionEntitiesCount: number;
14
+ path: string;
15
+ collection: EntityCollection;
16
+ };
@@ -0,0 +1,11 @@
1
+ import { DataType, Entity, Properties } from "@firecms/core";
2
+ type DataTypeMapping = {
3
+ from: DataType;
4
+ fromSubtype?: DataType;
5
+ to: DataType;
6
+ toSubtype?: DataType;
7
+ };
8
+ export declare function convertDataToEntity(data: Record<any, any>, idColumn: string | undefined, headersMapping: Record<string, string | null>, properties: Properties, propertiesMapping: Record<string, DataTypeMapping>, path: string): Entity<any>;
9
+ export declare function flattenEntry(obj: any, parent?: string): any;
10
+ export declare function processValueMapping(value: any, valueMapping?: DataTypeMapping): any;
11
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare function convertFileToJson(file: File): Promise<object[]>;
2
+ /**
3
+ * Take an object with keys of type `address.street`, `address.city` and
4
+ * convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
5
+ * @param flatObj
6
+ */
7
+ export declare function unflattenObject(flatObj: {
8
+ [key: string]: any;
9
+ }): {
10
+ [key: string]: any;
11
+ };
@@ -0,0 +1,2 @@
1
+ import { DataType } from "@firecms/core";
2
+ export declare function getInferenceType(value: any): DataType;
@@ -0,0 +1,3 @@
1
+ import { Properties } from "@firecms/core";
2
+ import { DataTypeMapping } from "../types";
3
+ export declare function getPropertiesMapping(originProperties: Properties, newProperties: Properties): Record<string, DataTypeMapping>;
@@ -0,0 +1,4 @@
1
+ export * from "./file_to_json";
2
+ export * from "./data";
3
+ export * from "./get_import_inference_type";
4
+ export * from "./get_properties_mapping";
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "@firecms/data_import_export",
3
+ "type": "module",
4
+ "version": "3.0.0-3.0.0-beta.4.pre.1.0",
5
+ "access": "public",
6
+ "main": "./dist/index.umd.js",
7
+ "module": "./dist/index.es.js",
8
+ "types": "./dist/index.d.ts",
9
+ "source": "src/index.ts",
10
+ "dependencies": {
11
+ "@firecms/core": "^3.0.0-3.0.0-beta.4.pre.1.0",
12
+ "@firecms/schema_inference": "^3.0.0-3.0.0-beta.4.pre.1.0",
13
+ "xlsx": "^0.18.5"
14
+ },
15
+ "peerDependencies": {
16
+ "firebase": "^10.7.1",
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "react-router": "^6.22.2",
20
+ "react-router-dom": "^6.22.2"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.es.js",
25
+ "require": "./dist/index.umd.js",
26
+ "types": "./dist/src/index.d.ts"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "scripts": {
31
+ "dev": "vite",
32
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
33
+ "prepublishOnly": "run-s build",
34
+ "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
35
+ "test": "jest"
36
+ },
37
+ "eslintConfig": {
38
+ "extends": [
39
+ "react-app",
40
+ "react-app/jest"
41
+ ]
42
+ },
43
+ "browserslist": {
44
+ "production": [
45
+ ">0.2%",
46
+ "not dead",
47
+ "not op_mini all"
48
+ ],
49
+ "development": [
50
+ "last 1 chrome version",
51
+ "last 1 firefox version",
52
+ "last 1 safari version"
53
+ ]
54
+ },
55
+ "devDependencies": {
56
+ "@jest/globals": "^29.7.0",
57
+ "@testing-library/jest-dom": "^6.4.2",
58
+ "@types/jest": "^29.5.12",
59
+ "@types/react": "^18.2.67",
60
+ "@types/react-dom": "^18.2.22",
61
+ "@typescript-eslint/eslint-plugin": "^7.3.1",
62
+ "@typescript-eslint/parser": "^7.3.1",
63
+ "@vitejs/plugin-react": "^4.2.1",
64
+ "babel-jest": "^29.7.0",
65
+ "eslint": "^8.57.0",
66
+ "eslint-config-standard": "^17.1.0",
67
+ "eslint-plugin-import": "^2.29.1",
68
+ "eslint-plugin-n": "^16.6.2",
69
+ "eslint-plugin-promise": "^6.1.1",
70
+ "eslint-plugin-react": "^7.34.1",
71
+ "eslint-plugin-react-hooks": "^4.6.0",
72
+ "jest": "^29.7.0",
73
+ "ts-jest": "^29.1.2",
74
+ "typescript": "^5.4.2",
75
+ "vite": "^5.1.6",
76
+ "vite-plugin-fonts": "^0.7.0"
77
+ },
78
+ "jest": {
79
+ "transform": {
80
+ "^.+\\.tsx?$": "ts-jest"
81
+ },
82
+ "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
83
+ "moduleFileExtensions": [
84
+ "ts",
85
+ "tsx",
86
+ "js",
87
+ "jsx",
88
+ "json",
89
+ "node"
90
+ ],
91
+ "moduleNameMapper": {
92
+ "\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
93
+ }
94
+ },
95
+ "files": [
96
+ "dist",
97
+ "src"
98
+ ],
99
+ "publishConfig": {
100
+ "access": "public"
101
+ },
102
+ "gitHead": "bfe875c02132b5763ac0c9d8c8a885c791bb5a24"
103
+ }
@@ -0,0 +1,128 @@
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 | null) => 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 | null) => void
107
+ }) {
108
+ return <div>
109
+ <Select
110
+ size={"small"}
111
+ value={idColumn ?? ""}
112
+ onChange={(event) => {
113
+ const value = event.target.value;
114
+ onChange(value === "none" ? null : value);
115
+ }}
116
+ renderValue={(value) => {
117
+ return <Typography variant={"body2"}>
118
+ {value !== "" ? value : "Autogenerate ID"}
119
+ </Typography>;
120
+ }}
121
+ label={"Column that will be used as ID for each document"}>
122
+ <SelectItem value={"none"}>Autogenerate ID</SelectItem>
123
+ {Object.entries(headersMapping).map(([key, value]) => {
124
+ return <SelectItem key={key} value={key}>{key}</SelectItem>;
125
+ })}
126
+ </Select>
127
+ </div>;
128
+ }
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,55 @@
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
+ }
@@ -0,0 +1,122 @@
1
+ import { DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
2
+ import { Button, 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
+ path,
9
+ importConfig,
10
+ collection,
11
+ onImportSuccess
12
+ }:
13
+ {
14
+ path: string,
15
+ importConfig: ImportConfig,
16
+ collection: C,
17
+ onImportSuccess: (collection: C) => void
18
+ }) {
19
+
20
+ const [errorSaving, setErrorSaving] = useState<Error | undefined>(undefined);
21
+ const dataSource = useDataSource();
22
+
23
+ const savingRef = useRef<boolean>(false);
24
+
25
+ const [processedEntities, setProcessedEntities] = useState<number>(0);
26
+
27
+ function save() {
28
+
29
+ if (savingRef.current)
30
+ return;
31
+
32
+ savingRef.current = true;
33
+
34
+ saveDataBatch(
35
+ dataSource,
36
+ collection,
37
+ path,
38
+ importConfig.entities,
39
+ 0,
40
+ 25,
41
+ setProcessedEntities
42
+ ).then(() => {
43
+ onImportSuccess(collection);
44
+ savingRef.current = false;
45
+ }).catch((e) => {
46
+ setErrorSaving(e);
47
+ savingRef.current = false;
48
+ });
49
+ }
50
+
51
+ useEffect(() => {
52
+ save();
53
+ }, []);
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
+
74
+ return (
75
+ <CenteredView className={"flex flex-col gap-4 items-center"}>
76
+ <CircularProgress/>
77
+
78
+ <Typography variant={"h6"}>
79
+ Saving data
80
+ </Typography>
81
+
82
+ <Typography variant={"body2"}>
83
+ {processedEntities}/{importConfig.entities.length} entities saved
84
+ </Typography>
85
+
86
+ <Typography variant={"caption"}>
87
+ Do not close this tab or the import will be interrupted.
88
+ </Typography>
89
+
90
+ </CenteredView>
91
+ );
92
+
93
+ }
94
+
95
+ function saveDataBatch(dataSource: DataSource,
96
+ collection: EntityCollection,
97
+ path: string,
98
+ data: Partial<Entity<any>>[],
99
+ offset = 0,
100
+ batchSize = 25,
101
+ onProgressUpdate: (progress: number) => void): Promise<void> {
102
+
103
+ console.debug("Saving imported data", offset, batchSize);
104
+
105
+ const batch = data.slice(offset, offset + batchSize);
106
+ return Promise.all(batch.map(d =>
107
+ dataSource.saveEntity({
108
+ path,
109
+ values: d.values,
110
+ entityId: d.id,
111
+ collection,
112
+ status: "new"
113
+ })))
114
+ .then(() => {
115
+ if (offset + batchSize < data.length) {
116
+ onProgressUpdate(offset + batchSize);
117
+ return saveDataBatch(dataSource, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
118
+ }
119
+ onProgressUpdate(data.length);
120
+ return Promise.resolve();
121
+ });
122
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./DataNewPropertiesMapping";
2
+ export * from "./ImportFileUpload";
3
+ export * from "./ImportNewPropertyFieldPreview";
4
+ export * from "./ImportSaveInProgress";