@firecms/data_import_export 3.0.0-canary.41 → 3.0.0-canary.42
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/dist/components/DataNewPropertiesMapping.d.ts +2 -1
- package/dist/components/ImportFileUpload.d.ts +1 -1
- package/dist/index.es.js +417 -330
- 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 +2 -0
- package/dist/utils/file_headers.d.ts +1 -0
- package/dist/utils/file_to_json.d.ts +6 -1
- package/package.json +4 -4
- package/src/components/DataNewPropertiesMapping.tsx +47 -37
- package/src/components/ImportFileUpload.tsx +12 -4
- package/src/components/ImportNewPropertyFieldPreview.tsx +7 -2
- package/src/export_import/ImportCollectionAction.tsx +2 -1
- package/src/hooks/useImportConfig.tsx +3 -0
- package/src/types/column_mapping.ts +4 -0
- package/src/utils/file_headers.ts +90 -0
- package/src/utils/file_to_json.ts +24 -11
@@ -14,6 +14,7 @@ import {
|
|
14
14
|
export interface DataPropertyMappingProps {
|
15
15
|
idColumn?: string;
|
16
16
|
headersMapping: Record<string, string | null>;
|
17
|
+
headingsOrder: string[];
|
17
18
|
originProperties: Record<string, Property>;
|
18
19
|
destinationProperties: Record<string, Property>;
|
19
20
|
onIdPropertyChanged: (value: string | null) => void;
|
@@ -28,12 +29,18 @@ export interface DataPropertyMappingProps {
|
|
28
29
|
export function DataNewPropertiesMapping({
|
29
30
|
idColumn,
|
30
31
|
headersMapping,
|
32
|
+
headingsOrder,
|
31
33
|
originProperties,
|
32
34
|
destinationProperties,
|
33
35
|
onIdPropertyChanged,
|
34
36
|
buildPropertyView,
|
35
37
|
}: DataPropertyMappingProps) {
|
36
38
|
|
39
|
+
console.log({
|
40
|
+
headersMapping,
|
41
|
+
headingsOrder,
|
42
|
+
})
|
43
|
+
|
37
44
|
return (
|
38
45
|
<>
|
39
46
|
|
@@ -41,6 +48,8 @@ export function DataNewPropertiesMapping({
|
|
41
48
|
headersMapping={headersMapping}
|
42
49
|
onChange={onIdPropertyChanged}/>
|
43
50
|
|
51
|
+
<div className={"h-4"}/>
|
52
|
+
|
44
53
|
<Table style={{
|
45
54
|
tableLayout: "fixed"
|
46
55
|
}}>
|
@@ -51,45 +60,45 @@ export function DataNewPropertiesMapping({
|
|
51
60
|
<TableCell header={true}>
|
52
61
|
</TableCell>
|
53
62
|
<TableCell header={true} style={{ width: "75%" }}>
|
54
|
-
Property
|
63
|
+
Map to Property
|
55
64
|
</TableCell>
|
56
65
|
</TableHeader>
|
57
66
|
<TableBody>
|
58
67
|
{destinationProperties &&
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
68
|
+
headingsOrder.map((importKey) => {
|
69
|
+
const mappedKey = headersMapping[importKey];
|
70
|
+
const propertyKey = headersMapping[importKey];
|
71
|
+
const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) as Property : null;
|
63
72
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
73
|
+
const originProperty = getPropertyInPath(originProperties, importKey) as Property | undefined;
|
74
|
+
const originDataType = originProperty ? (originProperty.dataType === "array" && typeof originProperty.of === "object"
|
75
|
+
? `${originProperty.dataType} - ${(originProperty.of as Property).dataType}`
|
76
|
+
: originProperty.dataType)
|
77
|
+
: undefined;
|
78
|
+
return <TableRow key={importKey} style={{ height: "90px" }}>
|
79
|
+
<TableCell style={{ width: "20%" }}>
|
80
|
+
<Typography variant={"body2"}>{importKey}</Typography>
|
81
|
+
{originProperty && <Typography
|
82
|
+
variant={"caption"}
|
83
|
+
color={"secondary"}
|
84
|
+
>{originDataType}</Typography>}
|
85
|
+
</TableCell>
|
86
|
+
<TableCell>
|
87
|
+
<ChevronRightIcon/>
|
88
|
+
</TableCell>
|
89
|
+
<TableCell className={importKey === idColumn ? "text-center" : undefined}
|
90
|
+
style={{ width: "75%" }}>
|
91
|
+
{buildPropertyView?.({
|
92
|
+
isIdColumn: importKey === idColumn,
|
93
|
+
property,
|
94
|
+
propertyKey,
|
95
|
+
importKey
|
96
|
+
})
|
97
|
+
}
|
98
|
+
</TableCell>
|
99
|
+
</TableRow>;
|
100
|
+
}
|
101
|
+
)}
|
93
102
|
</TableBody>
|
94
103
|
</Table>
|
95
104
|
</>
|
@@ -111,15 +120,16 @@ function IdSelectField({
|
|
111
120
|
value={idColumn ?? ""}
|
112
121
|
onChange={(event) => {
|
113
122
|
const value = event.target.value;
|
114
|
-
onChange(value === "
|
123
|
+
onChange(value === "__none__" ? null : value);
|
115
124
|
}}
|
125
|
+
placeholder={"Autogenerate ID"}
|
116
126
|
renderValue={(value) => {
|
117
127
|
return <Typography variant={"body2"}>
|
118
|
-
{value !== "" ? value : "Autogenerate ID"}
|
128
|
+
{value !== "__none__" ? value : "Autogenerate ID"}
|
119
129
|
</Typography>;
|
120
130
|
}}
|
121
131
|
label={"Column that will be used as ID for each document"}>
|
122
|
-
<SelectItem value={"
|
132
|
+
<SelectItem value={"__none__"}>Autogenerate ID</SelectItem>
|
123
133
|
{Object.entries(headersMapping).map(([key, value]) => {
|
124
134
|
return <SelectItem key={key} value={key}>{key}</SelectItem>;
|
125
135
|
})}
|
@@ -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
|
}
|
@@ -135,13 +135,14 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
|
|
135
135
|
<DataNewPropertiesMapping headersMapping={importConfig.headersMapping}
|
136
136
|
idColumn={importConfig.idColumn}
|
137
137
|
originProperties={importConfig.originProperties}
|
138
|
+
headingsOrder={importConfig.headingsOrder}
|
138
139
|
destinationProperties={properties}
|
139
140
|
onIdPropertyChanged={(value) => importConfig.setIdColumn(value ?? undefined)}
|
140
141
|
buildPropertyView={({
|
141
142
|
isIdColumn,
|
142
143
|
property,
|
143
144
|
propertyKey,
|
144
|
-
importKey
|
145
|
+
importKey,
|
145
146
|
}) => {
|
146
147
|
return <PropertyTreeSelect
|
147
148
|
selectedPropertyKey={propertyKey ?? ""}
|
@@ -9,6 +9,7 @@ export const useImportConfig = (): ImportConfig => {
|
|
9
9
|
const [importData, setImportData] = useState<object[]>([]);
|
10
10
|
const [entities, setEntities] = useState<Entity<any>[]>([]);
|
11
11
|
const [headersMapping, setHeadersMapping] = useState<Record<string, string | null>>({});
|
12
|
+
const [headingsOrder, setHeadingsOrder] = useState<string[]>([]);
|
12
13
|
const [originProperties, setOriginProperties] = useState<Record<string, Property>>({});
|
13
14
|
|
14
15
|
return {
|
@@ -20,6 +21,8 @@ export const useImportConfig = (): ImportConfig => {
|
|
20
21
|
setEntities,
|
21
22
|
importData,
|
22
23
|
setImportData,
|
24
|
+
headingsOrder: (headingsOrder ?? []).length > 0 ? headingsOrder : Object.keys(headersMapping),
|
25
|
+
setHeadingsOrder,
|
23
26
|
headersMapping,
|
24
27
|
setHeadersMapping,
|
25
28
|
originProperties,
|
@@ -22,6 +22,10 @@ export type ImportConfig = {
|
|
22
22
|
originProperties: Record<string, Property>;
|
23
23
|
setOriginProperties: React.Dispatch<React.SetStateAction<Record<string, Property>>>;
|
24
24
|
|
25
|
+
// unmapped headings order
|
26
|
+
headingsOrder: string[];
|
27
|
+
setHeadingsOrder: React.Dispatch<React.SetStateAction<string[]>>;
|
28
|
+
|
25
29
|
}
|
26
30
|
|
27
31
|
export type DataTypeMapping = {
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import * as XLSX from "xlsx";
|
2
|
+
export function getXLSXHeaders(sheet: any) {
|
3
|
+
let header = 0; let offset = 1;
|
4
|
+
const hdr = [];
|
5
|
+
const o:any = {};
|
6
|
+
if (sheet == null || sheet["!ref"] == null) return [];
|
7
|
+
const range = o.range !== undefined ? o.range : sheet["!ref"];
|
8
|
+
let r;
|
9
|
+
if (o.header === 1) header = 1;
|
10
|
+
else if (o.header === "A") header = 2;
|
11
|
+
else if (Array.isArray(o.header)) header = 3;
|
12
|
+
switch (typeof range) {
|
13
|
+
case "string":
|
14
|
+
r = safeDecodeRange(range);
|
15
|
+
break;
|
16
|
+
case "number":
|
17
|
+
r = safeDecodeRange(sheet["!ref"]);
|
18
|
+
r.s.r = range;
|
19
|
+
break;
|
20
|
+
default:
|
21
|
+
r = range;
|
22
|
+
}
|
23
|
+
if (header > 0) offset = 0;
|
24
|
+
const rr = XLSX.utils.encode_row(r.s.r);
|
25
|
+
const cols = new Array(r.e.c - r.s.c + 1);
|
26
|
+
for (let C = r.s.c; C <= r.e.c; ++C) {
|
27
|
+
cols[C] = XLSX.utils.encode_col(C);
|
28
|
+
const val = sheet[cols[C] + rr];
|
29
|
+
switch (header) {
|
30
|
+
case 1:
|
31
|
+
hdr.push(C);
|
32
|
+
break;
|
33
|
+
case 2:
|
34
|
+
hdr.push(cols[C]);
|
35
|
+
break;
|
36
|
+
case 3:
|
37
|
+
hdr.push(o.header[C - r.s.c]);
|
38
|
+
break;
|
39
|
+
default:
|
40
|
+
if (val === undefined) continue;
|
41
|
+
hdr.push(XLSX.utils.format_cell(val));
|
42
|
+
}
|
43
|
+
}
|
44
|
+
return hdr;
|
45
|
+
}
|
46
|
+
|
47
|
+
function safeDecodeRange(range:any) {
|
48
|
+
const o = {
|
49
|
+
s: {
|
50
|
+
c: 0,
|
51
|
+
r: 0
|
52
|
+
},
|
53
|
+
e: {
|
54
|
+
c: 0,
|
55
|
+
r: 0
|
56
|
+
}
|
57
|
+
};
|
58
|
+
let idx = 0; let i = 0; let cc = 0;
|
59
|
+
const len = range.length;
|
60
|
+
for (idx = 0; i < len; ++i) {
|
61
|
+
if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break;
|
62
|
+
idx = 26 * idx + cc;
|
63
|
+
}
|
64
|
+
o.s.c = --idx;
|
65
|
+
|
66
|
+
for (idx = 0; i < len; ++i) {
|
67
|
+
if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break;
|
68
|
+
idx = 10 * idx + cc;
|
69
|
+
}
|
70
|
+
o.s.r = --idx;
|
71
|
+
|
72
|
+
if (i === len || range.charCodeAt(++i) === 58) {
|
73
|
+
o.e.c = o.s.c;
|
74
|
+
o.e.r = o.s.r;
|
75
|
+
return o;
|
76
|
+
}
|
77
|
+
|
78
|
+
for (idx = 0; i !== len; ++i) {
|
79
|
+
if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break;
|
80
|
+
idx = 26 * idx + cc;
|
81
|
+
}
|
82
|
+
o.e.c = --idx;
|
83
|
+
|
84
|
+
for (idx = 0; i !== len; ++i) {
|
85
|
+
if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break;
|
86
|
+
idx = 10 * idx + cc;
|
87
|
+
}
|
88
|
+
o.e.r = --idx;
|
89
|
+
return o;
|
90
|
+
}
|
@@ -1,8 +1,13 @@
|
|
1
1
|
import * as XLSX from "xlsx";
|
2
|
+
import { getXLSXHeaders } from "./file_headers";
|
2
3
|
|
3
|
-
|
4
|
+
type ConversionResult = {
|
5
|
+
data: object[];
|
6
|
+
propertiesOrder: string[]
|
7
|
+
}
|
8
|
+
|
9
|
+
export function convertFileToJson(file: File): Promise<ConversionResult> {
|
4
10
|
return new Promise((resolve, reject) => {
|
5
|
-
// check if file is a JSON file
|
6
11
|
if (file.type === "application/json") {
|
7
12
|
console.debug("Converting JSON file to JSON", file.name);
|
8
13
|
const reader = new FileReader();
|
@@ -12,8 +17,14 @@ export function convertFileToJson(file: File): Promise<object[]> {
|
|
12
17
|
const jsonData = JSON.parse(data);
|
13
18
|
if (!Array.isArray(jsonData)) {
|
14
19
|
reject(new Error("JSON file should contain an array of objects"));
|
20
|
+
} else {
|
21
|
+
// Assuming all objects in the array have the same structure/order
|
22
|
+
const propertiesOrder = jsonData.length > 0 ? Object.keys(jsonData[0]) : [];
|
23
|
+
resolve({
|
24
|
+
data: jsonData,
|
25
|
+
propertiesOrder
|
26
|
+
});
|
15
27
|
}
|
16
|
-
resolve(jsonData);
|
17
28
|
} catch (e) {
|
18
29
|
console.error("Error parsing JSON file", e);
|
19
30
|
reject(e);
|
@@ -24,20 +35,22 @@ export function convertFileToJson(file: File): Promise<object[]> {
|
|
24
35
|
console.debug("Converting Excel file to JSON", file.name);
|
25
36
|
const reader = new FileReader();
|
26
37
|
reader.onload = function (e) {
|
27
|
-
|
28
38
|
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
29
|
-
const workbook = XLSX.read(data,
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
});
|
39
|
+
const workbook = XLSX.read(data, {
|
40
|
+
type: "array",
|
41
|
+
codepage: 65001,
|
42
|
+
cellDates: true,
|
43
|
+
});
|
35
44
|
const worksheetName = workbook.SheetNames[0];
|
36
45
|
const worksheet = workbook.Sheets[worksheetName];
|
37
46
|
const parsedData: Array<any> = XLSX.utils.sheet_to_json(worksheet);
|
47
|
+
const headers = getXLSXHeaders(worksheet);
|
38
48
|
const cleanedData = parsedData.map(mapJsonParse);
|
39
49
|
const jsonData = cleanedData.map(unflattenObject);
|
40
|
-
resolve(
|
50
|
+
resolve({
|
51
|
+
data: jsonData,
|
52
|
+
propertiesOrder: headers
|
53
|
+
});
|
41
54
|
};
|
42
55
|
reader.readAsArrayBuffer(file);
|
43
56
|
}
|