@firecms/data_import_export 3.0.0-canary.5 → 3.0.0-canary.51
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 +113 -21
- package/README.md +1 -1
- package/dist/components/DataNewPropertiesMapping.d.ts +3 -5
- package/dist/components/ImportFileUpload.d.ts +1 -1
- package/dist/export_import/BasicExportAction.d.ts +7 -0
- package/dist/export_import/ExportCollectionAction.d.ts +2 -1
- package/dist/export_import/ImportCollectionAction.d.ts +3 -1
- package/dist/export_import/export.d.ts +4 -4
- package/dist/export_import/index.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +901 -546
- 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/types/column_mapping.d.ts +5 -7
- package/dist/useImportExportPlugin.d.ts +2 -1
- package/dist/utils/data.d.ts +3 -10
- package/dist/utils/file_headers.d.ts +1 -0
- package/dist/utils/file_to_json.d.ts +6 -1
- package/dist/utils/get_properties_mapping.d.ts +0 -3
- package/dist/utils/index.d.ts +0 -1
- package/package.json +20 -18
- package/src/components/DataNewPropertiesMapping.tsx +155 -41
- package/src/components/ImportFileUpload.tsx +12 -4
- package/src/components/ImportNewPropertyFieldPreview.tsx +7 -2
- package/src/components/ImportSaveInProgress.tsx +24 -2
- package/src/export_import/BasicExportAction.tsx +137 -0
- package/src/export_import/ExportCollectionAction.tsx +15 -7
- package/src/export_import/ImportCollectionAction.tsx +52 -31
- package/src/export_import/export.ts +50 -30
- package/src/export_import/index.ts +4 -0
- package/src/hooks/useImportConfig.tsx +6 -0
- package/src/index.ts +1 -0
- package/src/types/column_mapping.ts +6 -6
- package/src/useImportExportPlugin.tsx +4 -3
- package/src/utils/data.ts +50 -127
- package/src/utils/file_headers.ts +90 -0
- package/src/utils/file_to_json.ts +33 -15
- package/src/utils/get_properties_mapping.ts +63 -59
- package/src/utils/index.ts +0 -1
package/dist/utils/data.d.ts
CHANGED
@@ -1,11 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
|
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>;
|
1
|
+
import { Entity, Properties, PropertyOrBuilder } from "@firecms/core";
|
2
|
+
export declare function convertDataToEntity(data: Record<any, any>, idColumn: string | undefined, headersMapping: Record<string, string | null>, properties: Properties, path: string, defaultValues: Record<string, any>): Entity<any>;
|
9
3
|
export declare function flattenEntry(obj: any, parent?: string): any;
|
10
|
-
export declare function processValueMapping(value: any,
|
11
|
-
export {};
|
4
|
+
export declare function processValueMapping(value: any, property?: PropertyOrBuilder): any;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare function getXLSXHeaders(sheet: any): any[];
|
@@ -1,4 +1,8 @@
|
|
1
|
-
|
1
|
+
type ConversionResult = {
|
2
|
+
data: object[];
|
3
|
+
propertiesOrder: string[];
|
4
|
+
};
|
5
|
+
export declare function convertFileToJson(file: File): Promise<ConversionResult>;
|
2
6
|
/**
|
3
7
|
* Take an object with keys of type `address.street`, `address.city` and
|
4
8
|
* convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
|
@@ -9,3 +13,4 @@ export declare function unflattenObject(flatObj: {
|
|
9
13
|
}): {
|
10
14
|
[key: string]: any;
|
11
15
|
};
|
16
|
+
export {};
|
package/dist/utils/index.d.ts
CHANGED
package/package.json
CHANGED
@@ -1,29 +1,31 @@
|
|
1
1
|
{
|
2
2
|
"name": "@firecms/data_import_export",
|
3
3
|
"type": "module",
|
4
|
-
"version": "3.0.0-canary.
|
4
|
+
"version": "3.0.0-canary.51",
|
5
5
|
"access": "public",
|
6
6
|
"main": "./dist/index.umd.js",
|
7
7
|
"module": "./dist/index.es.js",
|
8
8
|
"types": "./dist/index.d.ts",
|
9
9
|
"source": "src/index.ts",
|
10
10
|
"dependencies": {
|
11
|
-
"@firecms/core": "^3.0.0-canary.
|
12
|
-
"@firecms/
|
11
|
+
"@firecms/core": "^3.0.0-canary.51",
|
12
|
+
"@firecms/formex": "^3.0.0-canary.51",
|
13
|
+
"@firecms/schema_inference": "^3.0.0-canary.51",
|
14
|
+
"@firecms/ui": "^3.0.0-canary.51",
|
13
15
|
"xlsx": "^0.18.5"
|
14
16
|
},
|
15
17
|
"peerDependencies": {
|
16
18
|
"firebase": "^10.7.1",
|
17
19
|
"react": "^18.2.0",
|
18
20
|
"react-dom": "^18.2.0",
|
19
|
-
"react-router": "^6.
|
20
|
-
"react-router-dom": "^6.
|
21
|
+
"react-router": "^6.22.0",
|
22
|
+
"react-router-dom": "^6.22.0"
|
21
23
|
},
|
22
24
|
"exports": {
|
23
25
|
".": {
|
24
26
|
"import": "./dist/index.es.js",
|
25
27
|
"require": "./dist/index.umd.js",
|
26
|
-
"types": "./dist/
|
28
|
+
"types": "./dist/index.d.ts"
|
27
29
|
},
|
28
30
|
"./package.json": "./package.json"
|
29
31
|
},
|
@@ -54,25 +56,25 @@
|
|
54
56
|
},
|
55
57
|
"devDependencies": {
|
56
58
|
"@jest/globals": "^29.7.0",
|
57
|
-
"@testing-library/jest-dom": "^
|
59
|
+
"@testing-library/jest-dom": "^6.4.5",
|
58
60
|
"@types/jest": "^29.5.12",
|
59
|
-
"@types/react": "^18.
|
60
|
-
"@types/react-dom": "^18.
|
61
|
-
"@typescript-eslint/eslint-plugin": "^
|
62
|
-
"@typescript-eslint/parser": "^
|
61
|
+
"@types/react": "^18.3.1",
|
62
|
+
"@types/react-dom": "^18.3.0",
|
63
|
+
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
64
|
+
"@typescript-eslint/parser": "^7.8.0",
|
63
65
|
"@vitejs/plugin-react": "^4.2.1",
|
64
66
|
"babel-jest": "^29.7.0",
|
65
|
-
"eslint": "^8.
|
67
|
+
"eslint": "^8.57.0",
|
66
68
|
"eslint-config-standard": "^17.1.0",
|
67
69
|
"eslint-plugin-import": "^2.29.1",
|
68
|
-
"eslint-plugin-n": "^
|
70
|
+
"eslint-plugin-n": "^16.6.2",
|
69
71
|
"eslint-plugin-promise": "^6.1.1",
|
70
|
-
"eslint-plugin-react": "^7.
|
71
|
-
"eslint-plugin-react-hooks": "^4.6.
|
72
|
+
"eslint-plugin-react": "^7.34.1",
|
73
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
72
74
|
"jest": "^29.7.0",
|
73
75
|
"ts-jest": "^29.1.2",
|
74
|
-
"typescript": "^5.
|
75
|
-
"vite": "^5.
|
76
|
+
"typescript": "^5.4.5",
|
77
|
+
"vite": "^5.2.11",
|
76
78
|
"vite-plugin-fonts": "^0.7.0"
|
77
79
|
},
|
78
80
|
"jest": {
|
@@ -99,5 +101,5 @@
|
|
99
101
|
"publishConfig": {
|
100
102
|
"access": "public"
|
101
103
|
},
|
102
|
-
"gitHead": "
|
104
|
+
"gitHead": "d4d30347803b07a09f5b003eb63c78e46eb055fa"
|
103
105
|
}
|
@@ -1,6 +1,9 @@
|
|
1
|
-
import { getPropertyInPath,
|
1
|
+
import { getPropertyInPath, PropertiesOrBuilders, Property } from "@firecms/core";
|
2
2
|
import {
|
3
|
+
BooleanSwitchWithLabel,
|
3
4
|
ChevronRightIcon,
|
5
|
+
DateTimeField,
|
6
|
+
ExpandablePanel,
|
4
7
|
Select,
|
5
8
|
SelectItem,
|
6
9
|
Table,
|
@@ -8,15 +11,15 @@ import {
|
|
8
11
|
TableCell,
|
9
12
|
TableHeader,
|
10
13
|
TableRow,
|
14
|
+
TextField,
|
11
15
|
Typography
|
12
16
|
} from "@firecms/ui";
|
17
|
+
import { ImportConfig } from "../types";
|
18
|
+
import { getIn, setIn } from "@firecms/formex";
|
13
19
|
|
14
20
|
export interface DataPropertyMappingProps {
|
15
|
-
|
16
|
-
headersMapping: Record<string, string | null>;
|
17
|
-
originProperties: Record<string, Property>;
|
21
|
+
importConfig: ImportConfig;
|
18
22
|
destinationProperties: Record<string, Property>;
|
19
|
-
onIdPropertyChanged: (value: string) => void;
|
20
23
|
buildPropertyView?: (props: {
|
21
24
|
isIdColumn: boolean,
|
22
25
|
property: Property | null,
|
@@ -26,20 +29,24 @@ export interface DataPropertyMappingProps {
|
|
26
29
|
}
|
27
30
|
|
28
31
|
export function DataNewPropertiesMapping({
|
29
|
-
|
30
|
-
headersMapping,
|
31
|
-
originProperties,
|
32
|
+
importConfig,
|
32
33
|
destinationProperties,
|
33
|
-
|
34
|
-
buildPropertyView,
|
34
|
+
buildPropertyView
|
35
35
|
}: DataPropertyMappingProps) {
|
36
36
|
|
37
|
+
const headersMapping = importConfig.headersMapping;
|
38
|
+
const headingsOrder = importConfig.headingsOrder;
|
39
|
+
const idColumn = importConfig.idColumn;
|
40
|
+
const originProperties = importConfig.originProperties;
|
41
|
+
|
37
42
|
return (
|
38
43
|
<>
|
39
44
|
|
40
45
|
<IdSelectField idColumn={idColumn}
|
41
46
|
headersMapping={headersMapping}
|
42
|
-
onChange={
|
47
|
+
onChange={(value) => importConfig.setIdColumn(value ?? undefined)}/>
|
48
|
+
|
49
|
+
<div className={"h-4"}/>
|
43
50
|
|
44
51
|
<Table style={{
|
45
52
|
tableLayout: "fixed"
|
@@ -51,51 +58,113 @@ export function DataNewPropertiesMapping({
|
|
51
58
|
<TableCell header={true}>
|
52
59
|
</TableCell>
|
53
60
|
<TableCell header={true} style={{ width: "75%" }}>
|
54
|
-
Property
|
61
|
+
Map to Property
|
55
62
|
</TableCell>
|
56
63
|
</TableHeader>
|
57
64
|
<TableBody>
|
58
65
|
{destinationProperties &&
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
66
|
+
headingsOrder.map((importKey) => {
|
67
|
+
const mappedKey = headersMapping[importKey];
|
68
|
+
const propertyKey = headersMapping[importKey];
|
69
|
+
const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) as Property : null;
|
63
70
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
:
|
69
|
-
|
71
|
+
const originProperty = getPropertyInPath(originProperties, importKey) as Property | undefined;
|
72
|
+
const originDataType = originProperty
|
73
|
+
? (originProperty.dataType === "array" && typeof originProperty.of === "object"
|
74
|
+
? `${originProperty.dataType} - ${(originProperty.of as Property).dataType}`
|
75
|
+
: originProperty.dataType)
|
76
|
+
: undefined;
|
77
|
+
return <TableRow key={importKey} style={{ height: "90px" }}>
|
78
|
+
<TableCell style={{ width: "20%" }}>
|
79
|
+
<Typography variant={"body2"}>{importKey}</Typography>
|
80
|
+
{originProperty && <Typography
|
81
|
+
variant={"caption"}
|
82
|
+
color={"secondary"}
|
83
|
+
>{originDataType}</Typography>}
|
84
|
+
</TableCell>
|
85
|
+
<TableCell>
|
86
|
+
<ChevronRightIcon/>
|
87
|
+
</TableCell>
|
88
|
+
<TableCell className={importKey === idColumn ? "text-center" : undefined}
|
89
|
+
style={{ width: "75%" }}>
|
90
|
+
{buildPropertyView?.({
|
91
|
+
isIdColumn: importKey === idColumn,
|
92
|
+
property,
|
93
|
+
propertyKey,
|
94
|
+
importKey
|
95
|
+
})}
|
96
|
+
</TableCell>
|
97
|
+
</TableRow>;
|
98
|
+
}
|
99
|
+
)}
|
100
|
+
</TableBody>
|
101
|
+
</Table>
|
102
|
+
|
103
|
+
<ExpandablePanel title="Default values" initiallyExpanded={false} className={"p-4 mt-4"}>
|
104
|
+
|
105
|
+
<div className={"text-sm text-slate-500 dark:text-slate-300 font-medium ml-3.5 mb-1"}>
|
106
|
+
You can select a default value for unmapped columns and empty values:
|
107
|
+
</div>
|
108
|
+
<Table style={{
|
109
|
+
tableLayout: "fixed"
|
110
|
+
}}>
|
111
|
+
<TableHeader>
|
112
|
+
<TableCell header={true} style={{ width: "30%" }}>
|
113
|
+
Property
|
114
|
+
</TableCell>
|
115
|
+
<TableCell header={true}>
|
116
|
+
</TableCell>
|
117
|
+
<TableCell header={true} style={{ width: "65%" }}>
|
118
|
+
Default value
|
119
|
+
</TableCell>
|
120
|
+
</TableHeader>
|
121
|
+
<TableBody>
|
122
|
+
{destinationProperties &&
|
123
|
+
getAllPropertyKeys(destinationProperties).map((key) => {
|
124
|
+
const property = getPropertyInPath(destinationProperties, key);
|
125
|
+
if (typeof property !== "object" || property === null) {
|
126
|
+
return null;
|
127
|
+
}
|
128
|
+
if (!["number", "string", "boolean", "map"].includes(property.dataType)) {
|
129
|
+
return null;
|
130
|
+
}
|
131
|
+
return <TableRow key={key} style={{ height: "70px" }}>
|
70
132
|
<TableCell style={{ width: "20%" }}>
|
71
|
-
<Typography variant={"body2"}>{
|
72
|
-
{originProperty && <Typography
|
73
|
-
variant={"caption"}
|
74
|
-
color={"secondary"}
|
75
|
-
>{originDataType}</Typography>}
|
133
|
+
<Typography variant={"body2"}>{key}</Typography>
|
76
134
|
</TableCell>
|
77
135
|
<TableCell>
|
78
136
|
<ChevronRightIcon/>
|
79
137
|
</TableCell>
|
80
|
-
<TableCell className={
|
138
|
+
<TableCell className={key === idColumn ? "text-center" : undefined}
|
81
139
|
style={{ width: "75%" }}>
|
82
|
-
{
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
}
|
140
|
+
<DefaultValuesField property={property}
|
141
|
+
defaultValue={getIn(importConfig.defaultValues, key)}
|
142
|
+
onValueChange={(value) => {
|
143
|
+
const newValues = setIn(importConfig.defaultValues, key, value);
|
144
|
+
importConfig.setDefaultValues(newValues);
|
145
|
+
}}/>
|
89
146
|
</TableCell>
|
90
147
|
</TableRow>;
|
91
148
|
}
|
92
149
|
)}
|
93
|
-
|
94
|
-
|
150
|
+
</TableBody>
|
151
|
+
</Table>
|
152
|
+
</ExpandablePanel>
|
95
153
|
</>
|
96
154
|
);
|
97
155
|
}
|
98
156
|
|
157
|
+
function getAllPropertyKeys(properties: PropertiesOrBuilders, currentKey?: string): string[] {
|
158
|
+
return Object.entries(properties).reduce((acc, [key, property]) => {
|
159
|
+
const accumulatedKey = currentKey ? `${currentKey}.${key}` : key;
|
160
|
+
if (typeof property !== "function" && property.dataType === "map" && property.properties) {
|
161
|
+
const childProperties = getAllPropertyKeys(property.properties, accumulatedKey);
|
162
|
+
return [...acc, ...childProperties];
|
163
|
+
}
|
164
|
+
return [...acc, accumulatedKey];
|
165
|
+
}, [] as string[]);
|
166
|
+
}
|
167
|
+
|
99
168
|
function IdSelectField({
|
100
169
|
idColumn,
|
101
170
|
headersMapping,
|
@@ -103,25 +172,70 @@ function IdSelectField({
|
|
103
172
|
}: {
|
104
173
|
idColumn?: string,
|
105
174
|
headersMapping: Record<string, string | null>;
|
106
|
-
onChange: (value: string) => void
|
175
|
+
onChange: (value: string | null) => void
|
107
176
|
}) {
|
108
177
|
return <div>
|
109
178
|
<Select
|
110
179
|
size={"small"}
|
111
180
|
value={idColumn ?? ""}
|
112
181
|
onChange={(event) => {
|
113
|
-
|
182
|
+
const value = event.target.value;
|
183
|
+
onChange(value === "__none__" ? null : value);
|
114
184
|
}}
|
185
|
+
placeholder={"Autogenerate ID"}
|
115
186
|
renderValue={(value) => {
|
116
187
|
return <Typography variant={"body2"}>
|
117
|
-
{value !== "" ? value : "Autogenerate ID"}
|
188
|
+
{value !== "__none__" ? value : "Autogenerate ID"}
|
118
189
|
</Typography>;
|
119
190
|
}}
|
120
191
|
label={"Column that will be used as ID for each document"}>
|
121
|
-
<SelectItem value={""}>Autogenerate ID</SelectItem>
|
192
|
+
<SelectItem value={"__none__"}>Autogenerate ID</SelectItem>
|
122
193
|
{Object.entries(headersMapping).map(([key, value]) => {
|
123
194
|
return <SelectItem key={key} value={key}>{key}</SelectItem>;
|
124
195
|
})}
|
125
196
|
</Select>
|
126
197
|
</div>;
|
127
198
|
}
|
199
|
+
|
200
|
+
function DefaultValuesField({
|
201
|
+
property,
|
202
|
+
onValueChange,
|
203
|
+
defaultValue
|
204
|
+
}: { property: Property, onValueChange: (value: any) => void, defaultValue?: any }) {
|
205
|
+
if (property.dataType === "string") {
|
206
|
+
return <TextField size={"small"}
|
207
|
+
placeholder={"Default value"}
|
208
|
+
value={defaultValue ?? ""}
|
209
|
+
onChange={(event) => onValueChange(event.target.value)}/>;
|
210
|
+
} else if (property.dataType === "number") {
|
211
|
+
return <TextField size={"small"}
|
212
|
+
type={"number"}
|
213
|
+
value={defaultValue ?? ""}
|
214
|
+
placeholder={"Default value"}
|
215
|
+
onChange={(event) => onValueChange(event.target.value)}/>;
|
216
|
+
} else if (property.dataType === "boolean") {
|
217
|
+
return <BooleanSwitchWithLabel
|
218
|
+
value={defaultValue ?? null}
|
219
|
+
allowIndeterminate={true}
|
220
|
+
size={"small"}
|
221
|
+
onValueChange={(v: boolean | null) => onValueChange(v === null ? undefined : v)}
|
222
|
+
label={defaultValue === undefined
|
223
|
+
? "Do not set value"
|
224
|
+
: defaultValue === true
|
225
|
+
? "Set value to true"
|
226
|
+
: "Set value to false"}
|
227
|
+
/>
|
228
|
+
} else if (property.dataType === "date") {
|
229
|
+
return <DateTimeField
|
230
|
+
mode={property.mode ?? "date"}
|
231
|
+
size={"small"}
|
232
|
+
value={defaultValue ?? undefined}
|
233
|
+
onChange={(dateValue: Date | undefined) => {
|
234
|
+
onValueChange(dateValue);
|
235
|
+
}}
|
236
|
+
clearable={true}
|
237
|
+
/>
|
238
|
+
}
|
239
|
+
|
240
|
+
return null;
|
241
|
+
}
|
@@ -2,7 +2,9 @@ import { FileUpload, UploadIcon } from "@firecms/ui";
|
|
2
2
|
import { convertFileToJson } from "../utils/file_to_json";
|
3
3
|
import { useSnackbarController } from "@firecms/core";
|
4
4
|
|
5
|
-
export function ImportFileUpload({ onDataAdded }: {
|
5
|
+
export function ImportFileUpload({ onDataAdded }: {
|
6
|
+
onDataAdded: (data: object[], propertiesOrder?: string[]) => void
|
7
|
+
}) {
|
6
8
|
const snackbarController = useSnackbarController();
|
7
9
|
return <FileUpload
|
8
10
|
accept={{
|
@@ -22,12 +24,18 @@ export function ImportFileUpload({ onDataAdded }: { onDataAdded: (data: object[]
|
|
22
24
|
onFilesAdded={(files: File[]) => {
|
23
25
|
if (files.length > 0) {
|
24
26
|
convertFileToJson(files[0])
|
25
|
-
.then((
|
26
|
-
|
27
|
+
.then(({
|
28
|
+
data,
|
29
|
+
propertiesOrder
|
30
|
+
}) => {
|
31
|
+
onDataAdded(data, propertiesOrder);
|
27
32
|
})
|
28
33
|
.catch((error) => {
|
29
34
|
console.error("Error parsing file", error);
|
30
|
-
snackbarController.open({
|
35
|
+
snackbarController.open({
|
36
|
+
type: "error",
|
37
|
+
message: error.message
|
38
|
+
});
|
31
39
|
});
|
32
40
|
}
|
33
41
|
}}/>
|
@@ -1,5 +1,11 @@
|
|
1
1
|
import React from "react";
|
2
|
-
import {
|
2
|
+
import {
|
3
|
+
ErrorBoundary,
|
4
|
+
getFieldConfig,
|
5
|
+
Property,
|
6
|
+
PropertyConfigBadge,
|
7
|
+
useCustomizationController
|
8
|
+
} from "@firecms/core";
|
3
9
|
import { EditIcon, IconButton, TextField, } from "@firecms/ui";
|
4
10
|
|
5
11
|
export function ImportNewPropertyFieldPreview({
|
@@ -49,7 +55,6 @@ export function ImportNewPropertyFieldPreview({
|
|
49
55
|
|
50
56
|
</div>
|
51
57
|
|
52
|
-
|
53
58
|
</div>
|
54
59
|
</ErrorBoundary>
|
55
60
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
|
2
|
-
import { CenteredView, CircularProgress, Typography, } from "@firecms/ui";
|
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
|
|
@@ -17,7 +17,7 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
17
17
|
onImportSuccess: (collection: C) => void
|
18
18
|
}) {
|
19
19
|
|
20
|
-
|
20
|
+
const [errorSaving, setErrorSaving] = useState<Error | undefined>(undefined);
|
21
21
|
const dataSource = useDataSource();
|
22
22
|
|
23
23
|
const savingRef = useRef<boolean>(false);
|
@@ -42,6 +42,9 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
42
42
|
).then(() => {
|
43
43
|
onImportSuccess(collection);
|
44
44
|
savingRef.current = false;
|
45
|
+
}).catch((e) => {
|
46
|
+
setErrorSaving(e);
|
47
|
+
savingRef.current = false;
|
45
48
|
});
|
46
49
|
}
|
47
50
|
|
@@ -49,6 +52,25 @@ export function ImportSaveInProgress<C extends EntityCollection>
|
|
49
52
|
save();
|
50
53
|
}, []);
|
51
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
|
+
|
52
74
|
return (
|
53
75
|
<CenteredView className={"flex flex-col gap-4 items-center"}>
|
54
76
|
<CircularProgress/>
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import React, { useCallback } from "react";
|
2
|
+
|
3
|
+
import { Entity, ResolvedProperties } from "@firecms/core";
|
4
|
+
import {
|
5
|
+
BooleanSwitchWithLabel,
|
6
|
+
Button,
|
7
|
+
cn,
|
8
|
+
Dialog,
|
9
|
+
DialogActions,
|
10
|
+
DialogContent,
|
11
|
+
focusedMixin,
|
12
|
+
GetAppIcon,
|
13
|
+
IconButton,
|
14
|
+
Tooltip,
|
15
|
+
Typography,
|
16
|
+
} from "@firecms/ui";
|
17
|
+
import { downloadEntitiesExport } from "./export";
|
18
|
+
|
19
|
+
export type BasicExportActionProps = {
|
20
|
+
data: Entity<any>[];
|
21
|
+
properties: ResolvedProperties;
|
22
|
+
propertiesOrder?: string[];
|
23
|
+
}
|
24
|
+
|
25
|
+
export function BasicExportAction({
|
26
|
+
data,
|
27
|
+
properties,
|
28
|
+
propertiesOrder
|
29
|
+
}: BasicExportActionProps) {
|
30
|
+
|
31
|
+
const dateRef = React.useRef<Date>(new Date());
|
32
|
+
const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);
|
33
|
+
const [exportType, setExportType] = React.useState<"csv" | "json">("csv");
|
34
|
+
const [dateExportType, setDateExportType] = React.useState<"timestamp" | "string">("string");
|
35
|
+
|
36
|
+
const [open, setOpen] = React.useState(false);
|
37
|
+
|
38
|
+
const handleClickOpen = useCallback(() => {
|
39
|
+
setOpen(true);
|
40
|
+
}, [setOpen]);
|
41
|
+
|
42
|
+
const handleClose = useCallback(() => {
|
43
|
+
setOpen(false);
|
44
|
+
}, [setOpen]);
|
45
|
+
|
46
|
+
const onOkClicked = useCallback(() => {
|
47
|
+
downloadEntitiesExport(data, [], properties, propertiesOrder, "export.csv", flattenArrays, [], exportType, dateExportType);
|
48
|
+
handleClose();
|
49
|
+
}, []);
|
50
|
+
|
51
|
+
return <>
|
52
|
+
|
53
|
+
<Tooltip title={"Export"}>
|
54
|
+
<IconButton color={"primary"} onClick={handleClickOpen}>
|
55
|
+
<GetAppIcon/>
|
56
|
+
</IconButton>
|
57
|
+
</Tooltip>
|
58
|
+
|
59
|
+
<Dialog
|
60
|
+
open={open}
|
61
|
+
onOpenChange={setOpen}
|
62
|
+
maxWidth={"xl"}>
|
63
|
+
<DialogContent className={"flex flex-col gap-4 my-4"}>
|
64
|
+
|
65
|
+
<Typography variant={"h6"}>Export data</Typography>
|
66
|
+
|
67
|
+
<div>Download the the content of this table as a CSV</div>
|
68
|
+
|
69
|
+
<div className={"flex flex-row gap-4"}>
|
70
|
+
<div className={"p-4 flex flex-col"}>
|
71
|
+
<div className="flex items-center">
|
72
|
+
<input id="radio-csv" type="radio" value="csv" name="exportType"
|
73
|
+
checked={exportType === "csv"}
|
74
|
+
onChange={() => setExportType("csv")}
|
75
|
+
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
76
|
+
<label htmlFor="radio-csv"
|
77
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">CSV</label>
|
78
|
+
</div>
|
79
|
+
<div className="flex items-center">
|
80
|
+
<input id="radio-json" type="radio" value="json" name="exportType"
|
81
|
+
checked={exportType === "json"}
|
82
|
+
onChange={() => setExportType("json")}
|
83
|
+
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
84
|
+
<label htmlFor="radio-json"
|
85
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">JSON</label>
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div className={"p-4 flex flex-col"}>
|
90
|
+
<div className="flex items-center">
|
91
|
+
<input id="radio-timestamp" type="radio" value="timestamp" name="dateExportType"
|
92
|
+
checked={dateExportType === "timestamp"}
|
93
|
+
onChange={() => setDateExportType("timestamp")}
|
94
|
+
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
95
|
+
<label htmlFor="radio-timestamp"
|
96
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
97
|
+
timestamps ({dateRef.current.getTime()})</label>
|
98
|
+
</div>
|
99
|
+
<div className="flex items-center">
|
100
|
+
<input id="radio-string" type="radio" value="string" name="dateExportType"
|
101
|
+
checked={dateExportType === "string"}
|
102
|
+
onChange={() => setDateExportType("string")}
|
103
|
+
className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
|
104
|
+
<label htmlFor="radio-string"
|
105
|
+
className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
|
106
|
+
strings ({dateRef.current.toISOString()})</label>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
</div>
|
110
|
+
|
111
|
+
<BooleanSwitchWithLabel
|
112
|
+
size={"small"}
|
113
|
+
disabled={exportType !== "csv"}
|
114
|
+
value={flattenArrays}
|
115
|
+
onValueChange={setFlattenArrays}
|
116
|
+
label={"Flatten arrays"}/>
|
117
|
+
|
118
|
+
</DialogContent>
|
119
|
+
|
120
|
+
<DialogActions>
|
121
|
+
|
122
|
+
<Button onClick={handleClose}
|
123
|
+
variant={"text"}>
|
124
|
+
Cancel
|
125
|
+
</Button>
|
126
|
+
|
127
|
+
<Button variant="filled"
|
128
|
+
onClick={onOkClicked}>
|
129
|
+
Download
|
130
|
+
</Button>
|
131
|
+
|
132
|
+
</DialogActions>
|
133
|
+
|
134
|
+
</Dialog>
|
135
|
+
|
136
|
+
</>;
|
137
|
+
}
|