@firecms/data_import_export 3.0.0-canary.4 → 3.0.0-canary.41

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.
@@ -16,7 +16,7 @@ export interface DataPropertyMappingProps {
16
16
  headersMapping: Record<string, string | null>;
17
17
  originProperties: Record<string, Property>;
18
18
  destinationProperties: Record<string, Property>;
19
- onIdPropertyChanged: (value: string) => void;
19
+ onIdPropertyChanged: (value: string | null) => void;
20
20
  buildPropertyView?: (props: {
21
21
  isIdColumn: boolean,
22
22
  property: Property | null,
@@ -103,14 +103,15 @@ function IdSelectField({
103
103
  }: {
104
104
  idColumn?: string,
105
105
  headersMapping: Record<string, string | null>;
106
- onChange: (value: string) => void
106
+ onChange: (value: string | null) => void
107
107
  }) {
108
108
  return <div>
109
109
  <Select
110
110
  size={"small"}
111
111
  value={idColumn ?? ""}
112
112
  onChange={(event) => {
113
- onChange(event.target.value as string);
113
+ const value = event.target.value;
114
+ onChange(value === "none" ? null : value);
114
115
  }}
115
116
  renderValue={(value) => {
116
117
  return <Typography variant={"body2"}>
@@ -118,7 +119,7 @@ function IdSelectField({
118
119
  </Typography>;
119
120
  }}
120
121
  label={"Column that will be used as ID for each document"}>
121
- <SelectItem value={""}>Autogenerate ID</SelectItem>
122
+ <SelectItem value={"none"}>Autogenerate ID</SelectItem>
122
123
  {Object.entries(headersMapping).map(([key, value]) => {
123
124
  return <SelectItem key={key} value={key}>{key}</SelectItem>;
124
125
  })}
@@ -1,20 +1,23 @@
1
- import { CMSType, DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
2
- import { CenteredView, CircularProgress, Typography, } from "@firecms/ui";
1
+ import { DataSource, Entity, EntityCollection, useDataSource } from "@firecms/core";
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
 
6
6
  export function ImportSaveInProgress<C extends EntityCollection>
7
7
  ({
8
+ path,
8
9
  importConfig,
9
10
  collection,
10
11
  onImportSuccess
11
12
  }:
12
13
  {
14
+ path: string,
13
15
  importConfig: ImportConfig,
14
16
  collection: C,
15
17
  onImportSuccess: (collection: C) => void
16
18
  }) {
17
19
 
20
+ const [errorSaving, setErrorSaving] = useState<Error | undefined>(undefined);
18
21
  const dataSource = useDataSource();
19
22
 
20
23
  const savingRef = useRef<boolean>(false);
@@ -31,6 +34,7 @@ export function ImportSaveInProgress<C extends EntityCollection>
31
34
  saveDataBatch(
32
35
  dataSource,
33
36
  collection,
37
+ path,
34
38
  importConfig.entities,
35
39
  0,
36
40
  25,
@@ -38,6 +42,9 @@ export function ImportSaveInProgress<C extends EntityCollection>
38
42
  ).then(() => {
39
43
  onImportSuccess(collection);
40
44
  savingRef.current = false;
45
+ }).catch((e) => {
46
+ setErrorSaving(e);
47
+ savingRef.current = false;
41
48
  });
42
49
  }
43
50
 
@@ -45,6 +52,25 @@ export function ImportSaveInProgress<C extends EntityCollection>
45
52
  save();
46
53
  }, []);
47
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
+
48
74
  return (
49
75
  <CenteredView className={"flex flex-col gap-4 items-center"}>
50
76
  <CircularProgress/>
@@ -68,6 +94,7 @@ export function ImportSaveInProgress<C extends EntityCollection>
68
94
 
69
95
  function saveDataBatch(dataSource: DataSource,
70
96
  collection: EntityCollection,
97
+ path: string,
71
98
  data: Partial<Entity<any>>[],
72
99
  offset = 0,
73
100
  batchSize = 25,
@@ -78,7 +105,7 @@ function saveDataBatch(dataSource: DataSource,
78
105
  const batch = data.slice(offset, offset + batchSize);
79
106
  return Promise.all(batch.map(d =>
80
107
  dataSource.saveEntity({
81
- path: collection.path, // TODO: should check if this is correct, specially for subcollections
108
+ path,
82
109
  values: d.values,
83
110
  entityId: d.id,
84
111
  collection,
@@ -87,7 +114,7 @@ function saveDataBatch(dataSource: DataSource,
87
114
  .then(() => {
88
115
  if (offset + batchSize < data.length) {
89
116
  onProgressUpdate(offset + batchSize);
90
- return saveDataBatch(dataSource, collection, data, offset + batchSize, batchSize, onProgressUpdate);
117
+ return saveDataBatch(dataSource, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
91
118
  }
92
119
  onProgressUpdate(data.length);
93
120
  return Promise.resolve();
@@ -41,6 +41,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
41
41
  }: CollectionActionsProps<M, UserType, EntityCollection<M, any>> & {
42
42
  exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: EntityCollection }) => boolean;
43
43
  notAllowedView?: React.ReactNode;
44
+ onAnalyticsEvent?: (event: string, params?: any) => void;
44
45
  }) {
45
46
 
46
47
  const customizationController = useCustomizationController();
@@ -183,7 +184,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
183
184
  onChange={() => setExportType("csv")}
184
185
  className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
185
186
  <label htmlFor="radio-csv"
186
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">CSV</label>
187
+ className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">CSV</label>
187
188
  </div>
188
189
  <div className="flex items-center">
189
190
  <input id="radio-json" type="radio" value="json" name="exportType"
@@ -191,7 +192,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
191
192
  onChange={() => setExportType("json")}
192
193
  className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
193
194
  <label htmlFor="radio-json"
194
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">JSON</label>
195
+ className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">JSON</label>
195
196
  </div>
196
197
  </div>
197
198
 
@@ -202,7 +203,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
202
203
  onChange={() => setDateExportType("timestamp")}
203
204
  className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
204
205
  <label htmlFor="radio-timestamp"
205
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
206
+ className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
206
207
  timestamps ({dateRef.current.getTime()})</label>
207
208
  </div>
208
209
  <div className="flex items-center">
@@ -211,7 +212,7 @@ export function ExportCollectionAction<M extends Record<string, any>, UserType e
211
212
  onChange={() => setDateExportType("string")}
212
213
  className={cn(focusedMixin, "w-4 text-primary-dark bg-gray-100 border-gray-300 dark:bg-gray-700 dark:border-gray-600")}/>
213
214
  <label htmlFor="radio-string"
214
- className="p-2 text-sm font-medium text-gray-900 dark:text-gray-300">Dates as
215
+ className="p-2 text-sm font-medium text-gray-900 dark:text-slate-300">Dates as
215
216
  strings ({dateRef.current.toISOString()})</label>
216
217
  </div>
217
218
  </div>
@@ -2,13 +2,15 @@ import React, { useCallback, useEffect } from "react";
2
2
  import {
3
3
  CollectionActionsProps,
4
4
  EntityCollectionTable,
5
- PropertyConfigBadge,
6
5
  getFieldConfig,
7
6
  getPropertiesWithPropertiesOrder,
8
7
  getPropertyInPath,
8
+ PropertiesOrBuilders,
9
9
  Property,
10
+ PropertyConfigBadge,
10
11
  resolveCollection,
11
12
  ResolvedProperties,
13
+ slugify,
12
14
  useCustomizationController,
13
15
  User,
14
16
  useSelectionController,
@@ -40,8 +42,12 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
40
42
  collection,
41
43
  path,
42
44
  collectionEntitiesCount,
43
- }: CollectionActionsProps<M, UserType>
45
+ onAnalyticsEvent
46
+ }: CollectionActionsProps<M, UserType> & {
47
+ onAnalyticsEvent?: (event: string, params?: any) => void;
48
+ }
44
49
  ) {
50
+
45
51
  const customizationController = useCustomizationController();
46
52
 
47
53
  const snackbarController = useSnackbarController();
@@ -76,12 +82,11 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
76
82
  const originProperties = await buildEntityPropertiesFromData(data, getInferenceType);
77
83
  importConfig.setOriginProperties(originProperties);
78
84
 
79
- const headersMapping = buildHeadersMappingFromData(data);
85
+ const headersMapping = buildHeadersMappingFromData(data, collection?.properties);
80
86
  importConfig.setHeadersMapping(headersMapping);
81
87
  const firstKey = Object.keys(headersMapping)?.[0];
82
88
  if (firstKey?.includes("id") || firstKey?.includes("key")) {
83
- const idColumn = firstKey;
84
- importConfig.setIdColumn(idColumn);
89
+ importConfig.setIdColumn(firstKey);
85
90
  }
86
91
  }
87
92
  setTimeout(() => {
@@ -131,7 +136,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
131
136
  idColumn={importConfig.idColumn}
132
137
  originProperties={importConfig.originProperties}
133
138
  destinationProperties={properties}
134
- onIdPropertyChanged={(value) => importConfig.setIdColumn(value)}
139
+ onIdPropertyChanged={(value) => importConfig.setIdColumn(value ?? undefined)}
135
140
  buildPropertyView={({
136
141
  isIdColumn,
137
142
  property,
@@ -177,6 +182,7 @@ export function ImportCollectionAction<M extends Record<string, any>, UserType e
177
182
  {step === "import_data_saving" && importConfig &&
178
183
  <ImportSaveInProgress importConfig={importConfig}
179
184
  collection={collection}
185
+ path={path}
180
186
  onImportSuccess={(importedCollection) => {
181
187
  handleClose();
182
188
  snackbarController.open({
@@ -258,7 +264,7 @@ function PropertyTreeSelect({
258
264
  if (value === internalIDValue) {
259
265
  onIdSelected();
260
266
  onPropertySelected(null);
261
- } else if (value === "") {
267
+ } else if (value === "__do_not_import") {
262
268
  onPropertySelected(null);
263
269
  } else {
264
270
  onPropertySelected(value);
@@ -269,7 +275,7 @@ function PropertyTreeSelect({
269
275
  onValueChange={onSelectValueChange}
270
276
  renderValue={renderValue}>
271
277
 
272
- <SelectItem value={""}>
278
+ <SelectItem value={"__do_not_import"}>
273
279
  <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
274
280
  </SelectItem>
275
281
 
@@ -369,7 +375,7 @@ export function ImportDataPreview<M extends Record<string, any>>({
369
375
  }: {
370
376
  importConfig: ImportConfig,
371
377
  properties: ResolvedProperties<M>,
372
- propertiesOrder: Extract<keyof M, string>[]
378
+ propertiesOrder: Extract<keyof M, string>[],
373
379
  }) {
374
380
 
375
381
  useEffect(() => {
@@ -402,18 +408,34 @@ export function ImportDataPreview<M extends Record<string, any>>({
402
408
 
403
409
  }
404
410
 
405
- function buildHeadersMappingFromData(objArr: object[]) {
411
+ function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders<any>) {
406
412
  const headersMapping: Record<string, string> = {};
407
413
  objArr.filter(Boolean).forEach((obj) => {
408
414
  Object.keys(obj).forEach((key) => {
409
415
  // @ts-ignore
410
416
  const child = obj[key];
411
417
  if (typeof child === "object" && !Array.isArray(child)) {
412
- Object.entries(buildHeadersMappingFromData([child])).forEach(([subKey, mapping]) => {
418
+ const childProperty = properties?.[key];
419
+ const childProperties = childProperty && "properties" in childProperty ? childProperty.properties : undefined;
420
+ const childHeadersMapping = buildHeadersMappingFromData([child], childProperties);
421
+ Object.entries(childHeadersMapping).forEach(([subKey, mapping]) => {
413
422
  headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
414
423
  });
415
424
  }
416
- headersMapping[key] = key;
425
+
426
+ if (!properties) {
427
+ headersMapping[key] = key;
428
+ } else if (key in properties) {
429
+ headersMapping[key] = key;
430
+ } else {
431
+ const slug = slugify(key);
432
+ if (slug in properties) {
433
+ headersMapping[key] = slug;
434
+ } else {
435
+ headersMapping[key] = key;
436
+ }
437
+ }
438
+
417
439
  });
418
440
  });
419
441
  return headersMapping;
@@ -149,7 +149,7 @@ function processValueForExport(inputValue: any,
149
149
  } else {
150
150
  value = inputValue;
151
151
  }
152
- } else if (property.dataType === "reference" && inputValue.isEntityReference && inputValue.isEntityReference()) {
152
+ } else if (property.dataType === "reference" && inputValue && inputValue.isEntityReference && inputValue.isEntityReference()) {
153
153
  const ref = inputValue ? inputValue as EntityReference : undefined;
154
154
  value = ref ? ref.pathWithId : null;
155
155
  } else if (property.dataType === "date" && inputValue instanceof Date) {
@@ -9,8 +9,8 @@ import { ExportCollectionAction } from "./export_import/ExportCollectionAction";
9
9
  export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSPlugin<any, any, any, ImportExportPluginProps> {
10
10
 
11
11
  return useMemo(() => ({
12
- name: "Import/Export",
13
- collections: {
12
+ key: "import_export",
13
+ collectionView: {
14
14
  CollectionActions: [ImportCollectionAction, ExportCollectionAction],
15
15
  collectionActionsProps: props
16
16
  }
@@ -20,5 +20,6 @@ export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSP
20
20
  export type ImportExportPluginProps = {
21
21
  exportAllowed?: (props: ExportAllowedParams) => boolean;
22
22
  notAllowedView: React.ReactNode;
23
+ onAnalyticsEvent?: (event: string, params?: any) => void;
23
24
  }
24
25
  export type ExportAllowedParams = { collectionEntitiesCount: number, path: string, collection: EntityCollection };
package/src/utils/data.ts CHANGED
@@ -36,8 +36,21 @@ export function convertDataToEntity(data: Record<any, any>,
36
36
  })
37
37
  .reduce((acc, curr) => ({ ...acc, ...curr }), {});
38
38
  const values = unflattenObject(mappedKeysObject);
39
+ let id = idColumn ? data[idColumn] : undefined;
40
+ if (typeof id === "string") {
41
+ id = id.trim();
42
+ } else if (typeof id === "number") {
43
+ id = id.toString();
44
+ } else if (typeof id === "boolean") {
45
+ id = id.toString();
46
+ } else if (id instanceof Date) {
47
+ id = id.toISOString();
48
+ } else if (id && "toString" in id) {
49
+ id = id.toString();
50
+ }
51
+
39
52
  return {
40
- id: idColumn ? data[idColumn] : undefined,
53
+ id,
41
54
  values,
42
55
  path
43
56
  };
@@ -7,12 +7,17 @@ export function convertFileToJson(file: File): Promise<object[]> {
7
7
  console.debug("Converting JSON file to JSON", file.name);
8
8
  const reader = new FileReader();
9
9
  reader.onload = function (e) {
10
- const data = e.target?.result as string;
11
- const jsonData = JSON.parse(data);
12
- if (!Array.isArray(jsonData)) {
13
- reject(new Error("JSON file should contain an array of objects"));
10
+ try {
11
+ const data = e.target?.result as string;
12
+ const jsonData = JSON.parse(data);
13
+ if (!Array.isArray(jsonData)) {
14
+ reject(new Error("JSON file should contain an array of objects"));
15
+ }
16
+ resolve(jsonData);
17
+ } catch (e) {
18
+ console.error("Error parsing JSON file", e);
19
+ reject(e);
14
20
  }
15
- resolve(jsonData);
16
21
  };
17
22
  reader.readAsText(file);
18
23
  } else {