@firecms/data_import_export 3.0.0-canary.42 → 3.0.0-canary.43

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.
@@ -1,4 +1,3 @@
1
1
  export * from "./file_to_json";
2
2
  export * from "./data";
3
3
  export * from "./get_import_inference_type";
4
- export * from "./get_properties_mapping";
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@firecms/data_import_export",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.42",
4
+ "version": "3.0.0-canary.43",
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.42",
12
- "@firecms/schema_inference": "^3.0.0-canary.42",
11
+ "@firecms/core": "^3.0.0-canary.43",
12
+ "@firecms/formex": "^3.0.0-canary.43",
13
+ "@firecms/schema_inference": "^3.0.0-canary.43",
14
+ "@firecms/ui": "^3.0.0-canary.43",
13
15
  "xlsx": "^0.18.5"
14
16
  },
15
17
  "peerDependencies": {
@@ -56,10 +58,10 @@
56
58
  "@jest/globals": "^29.7.0",
57
59
  "@testing-library/jest-dom": "^6.4.2",
58
60
  "@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",
61
+ "@types/react": "^18.2.79",
62
+ "@types/react-dom": "^18.2.25",
63
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
64
+ "@typescript-eslint/parser": "^7.7.0",
63
65
  "@vitejs/plugin-react": "^4.2.1",
64
66
  "babel-jest": "^29.7.0",
65
67
  "eslint": "^8.57.0",
@@ -71,8 +73,8 @@
71
73
  "eslint-plugin-react-hooks": "^4.6.0",
72
74
  "jest": "^29.7.0",
73
75
  "ts-jest": "^29.1.2",
74
- "typescript": "^5.4.2",
75
- "vite": "^5.2.3",
76
+ "typescript": "^5.4.5",
77
+ "vite": "^5.2.9",
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": "d6a2f28e93d3c532dd6efacfccffb91383d5330e"
104
+ "gitHead": "fedcb0d43c504245dd76b702e4ab4479fa8592df"
103
105
  }
@@ -1,6 +1,9 @@
1
- import { getPropertyInPath, Property, } from "@firecms/core";
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,16 +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
- idColumn?: string;
16
- headersMapping: Record<string, string | null>;
17
- headingsOrder: string[];
18
- originProperties: Record<string, Property>;
21
+ importConfig: ImportConfig;
19
22
  destinationProperties: Record<string, Property>;
20
- onIdPropertyChanged: (value: string | null) => void;
21
23
  buildPropertyView?: (props: {
22
24
  isIdColumn: boolean,
23
25
  property: Property | null,
@@ -27,26 +29,22 @@ export interface DataPropertyMappingProps {
27
29
  }
28
30
 
29
31
  export function DataNewPropertiesMapping({
30
- idColumn,
31
- headersMapping,
32
- headingsOrder,
33
- originProperties,
32
+ importConfig,
34
33
  destinationProperties,
35
- onIdPropertyChanged,
36
- buildPropertyView,
34
+ buildPropertyView
37
35
  }: DataPropertyMappingProps) {
38
36
 
39
- console.log({
40
- headersMapping,
41
- headingsOrder,
42
- })
37
+ const headersMapping = importConfig.headersMapping;
38
+ const headingsOrder = importConfig.headingsOrder;
39
+ const idColumn = importConfig.idColumn;
40
+ const originProperties = importConfig.originProperties;
43
41
 
44
42
  return (
45
43
  <>
46
44
 
47
45
  <IdSelectField idColumn={idColumn}
48
46
  headersMapping={headersMapping}
49
- onChange={onIdPropertyChanged}/>
47
+ onChange={(value) => importConfig.setIdColumn(value ?? undefined)}/>
50
48
 
51
49
  <div className={"h-4"}/>
52
50
 
@@ -71,7 +69,8 @@ export function DataNewPropertiesMapping({
71
69
  const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) as Property : null;
72
70
 
73
71
  const originProperty = getPropertyInPath(originProperties, importKey) as Property | undefined;
74
- const originDataType = originProperty ? (originProperty.dataType === "array" && typeof originProperty.of === "object"
72
+ const originDataType = originProperty
73
+ ? (originProperty.dataType === "array" && typeof originProperty.of === "object"
75
74
  ? `${originProperty.dataType} - ${(originProperty.of as Property).dataType}`
76
75
  : originProperty.dataType)
77
76
  : undefined;
@@ -93,18 +92,79 @@ export function DataNewPropertiesMapping({
93
92
  property,
94
93
  propertyKey,
95
94
  importKey
96
- })
97
- }
95
+ })}
98
96
  </TableCell>
99
97
  </TableRow>;
100
98
  }
101
99
  )}
102
100
  </TableBody>
103
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" }}>
132
+ <TableCell style={{ width: "20%" }}>
133
+ <Typography variant={"body2"}>{key}</Typography>
134
+ </TableCell>
135
+ <TableCell>
136
+ <ChevronRightIcon/>
137
+ </TableCell>
138
+ <TableCell className={key === idColumn ? "text-center" : undefined}
139
+ style={{ width: "75%" }}>
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
+ }}/>
146
+ </TableCell>
147
+ </TableRow>;
148
+ }
149
+ )}
150
+ </TableBody>
151
+ </Table>
152
+ </ExpandablePanel>
104
153
  </>
105
154
  );
106
155
  }
107
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
+
108
168
  function IdSelectField({
109
169
  idColumn,
110
170
  headersMapping,
@@ -136,3 +196,46 @@ function IdSelectField({
136
196
  </Select>
137
197
  </div>;
138
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
+ }
@@ -36,6 +36,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
36
36
  collection: inputCollection,
37
37
  path: inputPath,
38
38
  collectionEntitiesCount,
39
+ onAnalyticsEvent,
39
40
  exportAllowed,
40
41
  notAllowedView
41
42
  }: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
@@ -123,6 +124,9 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
123
124
  const doDownload = useCallback(async (collection: ResolvedEntityCollection<M>,
124
125
  exportConfig: ExportConfig<any> | undefined) => {
125
126
 
127
+ onAnalyticsEvent?.("export_collection", {
128
+ collection: collection.path
129
+ });
126
130
  setDataLoading(true);
127
131
  dataSource.fetchCollection<M>({
128
132
  path,
@@ -136,6 +140,9 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
136
140
  ...collection.additionalFields?.map(field => field.key) ?? []
137
141
  ];
138
142
  downloadExport(data, additionalData, collection, flattenArrays, additionalHeaders, exportType, dateExportType);
143
+ onAnalyticsEvent?.("export_collection_success", {
144
+ collection: collection.path
145
+ });
139
146
  })
140
147
  .catch((e) => {
141
148
  console.error("Error loading export data", e);
@@ -143,7 +150,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
143
150
  })
144
151
  .finally(() => setDataLoading(false));
145
152
 
146
- }, [dataSource, path, fetchAdditionalFields, flattenArrays, exportType, dateExportType]);
153
+ }, [onAnalyticsEvent, dataSource, path, fetchAdditionalFields, flattenArrays, exportType, dateExportType]);
147
154
 
148
155
  const onOkClicked = useCallback(() => {
149
156
  doDownload(collection, exportConfig);
@@ -32,7 +32,7 @@ import {
32
32
  } from "@firecms/ui";
33
33
  import { buildEntityPropertiesFromData } from "@firecms/schema_inference";
34
34
  import { useImportConfig } from "../hooks";
35
- import { convertDataToEntity, getInferenceType, getPropertiesMapping } from "../utils";
35
+ import { convertDataToEntity, getInferenceType } from "../utils";
36
36
  import { DataNewPropertiesMapping, ImportFileUpload, ImportSaveInProgress } from "../components";
37
37
  import { ImportConfig } from "../types";
38
38
 
@@ -60,20 +60,23 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
60
60
 
61
61
  const handleClickOpen = useCallback(() => {
62
62
  setOpen(true);
63
+ onAnalyticsEvent?.("import_open");
63
64
  setStep("initial");
64
- }, [setOpen]);
65
+ }, [onAnalyticsEvent]);
65
66
 
66
67
  const handleClose = useCallback(() => {
67
68
  setOpen(false);
68
69
  }, [setOpen]);
69
70
 
70
71
  const onMappingComplete = useCallback(() => {
72
+ onAnalyticsEvent?.("import_mapping_complete");
71
73
  setStep("preview");
72
- }, []);
74
+ }, [onAnalyticsEvent]);
73
75
 
74
76
  const onPreviewComplete = useCallback(() => {
77
+ onAnalyticsEvent?.("import_data_save");
75
78
  setStep("import_data_saving");
76
- }, []);
79
+ }, [onAnalyticsEvent]);
77
80
 
78
81
  const onDataAdded = async (data: object[]) => {
79
82
  importConfig.setImportData(data);
@@ -90,6 +93,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
90
93
  }
91
94
  }
92
95
  setTimeout(() => {
96
+ onAnalyticsEvent?.("import_data_added");
93
97
  setStep("mapping");
94
98
  }, 100);
95
99
  // setStep("mapping");
@@ -109,6 +113,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
109
113
  if (collection.collectionGroup) {
110
114
  return null;
111
115
  }
116
+
112
117
  return <>
113
118
 
114
119
  <Tooltip title={"Import"}>
@@ -131,13 +136,9 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
131
136
  </>}
132
137
 
133
138
  {step === "mapping" && <>
134
- <Typography variant={"h6"}>Map fields</Typography>
135
- <DataNewPropertiesMapping headersMapping={importConfig.headersMapping}
136
- idColumn={importConfig.idColumn}
137
- originProperties={importConfig.originProperties}
138
- headingsOrder={importConfig.headingsOrder}
139
+ <Typography variant={"h6"} className={"ml-3.5"}>Map fields</Typography>
140
+ <DataNewPropertiesMapping importConfig={importConfig}
139
141
  destinationProperties={properties}
140
- onIdPropertyChanged={(value) => importConfig.setIdColumn(value ?? undefined)}
141
142
  buildPropertyView={({
142
143
  isIdColumn,
143
144
  property,
@@ -154,6 +155,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
154
155
  }}
155
156
  onPropertySelected={(newPropertyKey) => {
156
157
 
158
+ onAnalyticsEvent?.("import_mapping_field_updated");
157
159
  const newHeadersMapping: Record<string, string | null> = Object.entries(importConfig.headersMapping)
158
160
  .map(([currentImportKey, currentPropertyKey]) => {
159
161
  if (currentPropertyKey === newPropertyKey) {
@@ -254,14 +256,15 @@ function PropertyTreeSelect({
254
256
  }
255
257
 
256
258
  if (!selectedPropertyKey || !selectedProperty) {
257
- return <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>;
259
+ return <Typography variant={"body2"} color="disabled" className={"p-4"}>Do not import this
260
+ property</Typography>;
258
261
  }
259
262
 
260
263
  return <PropertySelectEntry propertyKey={selectedPropertyKey}
261
264
  property={selectedProperty as Property}/>;
262
265
  }, [selectedProperty]);
263
266
 
264
- const onSelectValueChange = useCallback((value: string) => {
267
+ const onSelectValueChange = (value: string) => {
265
268
  if (value === internalIDValue) {
266
269
  onIdSelected();
267
270
  onPropertySelected(null);
@@ -270,14 +273,14 @@ function PropertyTreeSelect({
270
273
  } else {
271
274
  onPropertySelected(value);
272
275
  }
273
- }, []);
276
+ };
274
277
 
275
278
  return <Select value={isIdColumn ? internalIDValue : (selectedPropertyKey ?? undefined)}
276
279
  onValueChange={onSelectValueChange}
277
280
  renderValue={renderValue}>
278
281
 
279
282
  <SelectItem value={"__do_not_import"}>
280
- <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
283
+ <Typography variant={"body2"} color={"disabled"} className={"p-4"}>Do not import this property</Typography>
281
284
  </SelectItem>
282
285
 
283
286
  <SelectItem value={internalIDValue}>
@@ -380,8 +383,11 @@ export function ImportDataPreview<M extends Record<string, any>>({
380
383
  }) {
381
384
 
382
385
  useEffect(() => {
383
- const propertiesMapping = getPropertiesMapping(importConfig.originProperties, properties);
384
- const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, propertiesMapping, "TEMP_PATH"));
386
+ const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, "TEMP_PATH", importConfig.defaultValues));
387
+ console.log("Mapped data", {
388
+ importConfig,
389
+ mappedData
390
+ })
385
391
  importConfig.setEntities(mappedData);
386
392
  }, []);
387
393
 
@@ -401,10 +407,6 @@ export function ImportDataPreview<M extends Record<string, any>>({
401
407
  filterable={false}
402
408
  sortable={false}
403
409
  selectionController={selectionController}
404
- displayedColumnIds={propertiesOrder.map(p => ({
405
- key: p,
406
- disabled: false
407
- }))}
408
410
  properties={properties}/>
409
411
 
410
412
  }
@@ -5,6 +5,7 @@ import { ImportConfig } from "../types";
5
5
  export const useImportConfig = (): ImportConfig => {
6
6
 
7
7
  const [inUse, setInUse] = useState<boolean>(false);
8
+ const [defaultValues, setDefaultValues] = useState<Record<string, any>>({});
8
9
  const [idColumn, setIdColumn] = useState<string | undefined>();
9
10
  const [importData, setImportData] = useState<object[]>([]);
10
11
  const [entities, setEntities] = useState<Entity<any>[]>([]);
@@ -27,5 +28,7 @@ export const useImportConfig = (): ImportConfig => {
27
28
  setHeadersMapping,
28
29
  originProperties,
29
30
  setOriginProperties,
31
+ defaultValues,
32
+ setDefaultValues
30
33
  };
31
34
  };
@@ -26,11 +26,7 @@ export type ImportConfig = {
26
26
  headingsOrder: string[];
27
27
  setHeadingsOrder: React.Dispatch<React.SetStateAction<string[]>>;
28
28
 
29
- }
29
+ defaultValues: Record<string, any>;
30
+ setDefaultValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
30
31
 
31
- export type DataTypeMapping = {
32
- from: DataType;
33
- fromSubtype?: DataType;
34
- to: DataType;
35
- toSubtype?: DataType;
36
32
  }