@firecms/data_import_export 3.0.0-canary.7 → 3.0.0-canary.8

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,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
- console.log("ImportSaveInProgress", path)
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/>
@@ -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,
@@ -259,7 +264,7 @@ function PropertyTreeSelect({
259
264
  if (value === internalIDValue) {
260
265
  onIdSelected();
261
266
  onPropertySelected(null);
262
- } else if (value === "") {
267
+ } else if (value === "__do_not_import") {
263
268
  onPropertySelected(null);
264
269
  } else {
265
270
  onPropertySelected(value);
@@ -270,7 +275,7 @@ function PropertyTreeSelect({
270
275
  onValueChange={onSelectValueChange}
271
276
  renderValue={renderValue}>
272
277
 
273
- <SelectItem value={""}>
278
+ <SelectItem value={"__do_not_import"}>
274
279
  <Typography variant={"body2"} className={"p-4"}>Do not import this property</Typography>
275
280
  </SelectItem>
276
281
 
@@ -370,7 +375,7 @@ export function ImportDataPreview<M extends Record<string, any>>({
370
375
  }: {
371
376
  importConfig: ImportConfig,
372
377
  properties: ResolvedProperties<M>,
373
- propertiesOrder: Extract<keyof M, string>[]
378
+ propertiesOrder: Extract<keyof M, string>[],
374
379
  }) {
375
380
 
376
381
  useEffect(() => {
@@ -403,18 +408,34 @@ export function ImportDataPreview<M extends Record<string, any>>({
403
408
 
404
409
  }
405
410
 
406
- function buildHeadersMappingFromData(objArr: object[]) {
411
+ function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders<any>) {
407
412
  const headersMapping: Record<string, string> = {};
408
413
  objArr.filter(Boolean).forEach((obj) => {
409
414
  Object.keys(obj).forEach((key) => {
410
415
  // @ts-ignore
411
416
  const child = obj[key];
412
417
  if (typeof child === "object" && !Array.isArray(child)) {
413
- 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]) => {
414
422
  headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`;
415
423
  });
416
424
  }
417
- 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
+
418
439
  });
419
440
  });
420
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) {
@@ -10,7 +10,7 @@ export function useImportExportPlugin(props?: ImportExportPluginProps): FireCMSP
10
10
 
11
11
  return useMemo(() => ({
12
12
  name: "Import/Export",
13
- collections: {
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
  };