@firecms/collection_editor 3.0.0-alpha.8 → 3.0.0-alpha.80

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.
Files changed (138) hide show
  1. package/dist/ConfigControllerProvider.d.ts +37 -0
  2. package/dist/index.d.ts +11 -0
  3. package/dist/index.es.js +5257 -0
  4. package/dist/index.es.js.map +1 -0
  5. package/dist/index.umd.js +4 -0
  6. package/dist/index.umd.js.map +1 -0
  7. package/dist/types/collection_editor_controller.d.ts +35 -0
  8. package/dist/types/collection_inference.d.ts +2 -0
  9. package/dist/types/config_controller.d.ts +51 -0
  10. package/dist/types/config_permissions.d.ts +19 -0
  11. package/dist/types/persisted_collection.d.ts +6 -0
  12. package/dist/ui/CollectionViewHeaderAction.d.ts +10 -0
  13. package/dist/ui/EditorCollectionAction.d.ts +2 -0
  14. package/dist/ui/HomePageEditorCollectionAction.d.ts +2 -0
  15. package/dist/ui/MissingReferenceWidget.d.ts +3 -0
  16. package/dist/ui/NewCollectionButton.d.ts +1 -0
  17. package/dist/ui/NewCollectionCard.d.ts +2 -0
  18. package/dist/ui/PropertyAddColumnComponent.d.ts +6 -0
  19. package/dist/ui/RootCollectionSuggestions.d.ts +1 -0
  20. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +10 -0
  21. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +38 -0
  22. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +15 -0
  23. package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +20 -0
  24. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +14 -0
  25. package/dist/ui/collection_editor/EntityCustomViewsSelectDialog.d.ts +4 -0
  26. package/dist/ui/collection_editor/EnumForm.d.ts +13 -0
  27. package/dist/ui/collection_editor/GetCodeDialog.d.ts +5 -0
  28. package/dist/ui/collection_editor/PropertyEditView.d.ts +40 -0
  29. package/dist/ui/collection_editor/PropertyFieldPreview.d.ts +15 -0
  30. package/dist/ui/collection_editor/PropertySelectItem.d.ts +8 -0
  31. package/dist/ui/collection_editor/PropertyTree.d.ts +32 -0
  32. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +12 -0
  33. package/dist/ui/collection_editor/UnsavedChangesDialog.d.ts +9 -0
  34. package/dist/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +7 -0
  35. package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +7 -0
  36. package/dist/ui/collection_editor/import/clean_import_data.d.ts +7 -0
  37. package/dist/ui/collection_editor/properties/BlockPropertyField.d.ts +8 -0
  38. package/dist/ui/collection_editor/properties/BooleanPropertyField.d.ts +3 -0
  39. package/dist/ui/collection_editor/properties/CommonPropertyFields.d.ts +11 -0
  40. package/dist/ui/collection_editor/properties/DateTimePropertyField.d.ts +3 -0
  41. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +8 -0
  42. package/dist/ui/collection_editor/properties/FieldHelperView.d.ts +4 -0
  43. package/dist/ui/collection_editor/properties/KeyValuePropertyField.d.ts +3 -0
  44. package/dist/ui/collection_editor/properties/MapPropertyField.d.ts +8 -0
  45. package/dist/ui/collection_editor/properties/NumberPropertyField.d.ts +3 -0
  46. package/dist/ui/collection_editor/properties/ReferencePropertyField.d.ts +13 -0
  47. package/dist/ui/collection_editor/properties/RepeatPropertyField.d.ts +10 -0
  48. package/dist/ui/collection_editor/properties/StoragePropertyField.d.ts +5 -0
  49. package/dist/ui/collection_editor/properties/StringPropertyField.d.ts +5 -0
  50. package/dist/ui/collection_editor/properties/UrlPropertyField.d.ts +4 -0
  51. package/dist/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.d.ts +3 -0
  52. package/dist/ui/collection_editor/properties/validation/ArrayPropertyValidation.d.ts +5 -0
  53. package/dist/ui/collection_editor/properties/validation/GeneralPropertyValidation.d.ts +4 -0
  54. package/dist/ui/collection_editor/properties/validation/NumberPropertyValidation.d.ts +3 -0
  55. package/dist/ui/collection_editor/properties/validation/StringPropertyValidation.d.ts +11 -0
  56. package/dist/ui/collection_editor/properties/validation/ValidationPanel.d.ts +2 -0
  57. package/dist/ui/collection_editor/templates/blog_template.d.ts +2 -0
  58. package/dist/ui/collection_editor/templates/pages_template.d.ts +2 -0
  59. package/dist/ui/collection_editor/templates/products_template.d.ts +2 -0
  60. package/dist/ui/collection_editor/templates/users_template.d.ts +2 -0
  61. package/dist/ui/collection_editor/util.d.ts +4 -0
  62. package/dist/ui/collection_editor/utils/strings.d.ts +1 -0
  63. package/dist/ui/collection_editor/utils/supported_fields.d.ts +3 -0
  64. package/dist/ui/collection_editor/utils/update_property_for_widget.d.ts +2 -0
  65. package/dist/ui/collection_editor/utils/useTraceUpdate.d.ts +1 -0
  66. package/dist/useCollectionEditorController.d.ts +6 -0
  67. package/dist/useCollectionEditorPlugin.d.ts +46 -0
  68. package/dist/useCollectionsConfigController.d.ts +6 -0
  69. package/dist/utils/arrays.d.ts +1 -0
  70. package/dist/utils/entities.d.ts +3 -0
  71. package/package.json +24 -27
  72. package/src/ConfigControllerProvider.tsx +330 -0
  73. package/src/index.ts +35 -0
  74. package/src/types/collection_editor_controller.tsx +42 -0
  75. package/src/types/collection_inference.ts +3 -0
  76. package/src/types/config_controller.tsx +60 -0
  77. package/src/types/config_permissions.ts +20 -0
  78. package/src/types/persisted_collection.ts +9 -0
  79. package/src/ui/CollectionViewHeaderAction.tsx +43 -0
  80. package/src/ui/EditorCollectionAction.tsx +109 -0
  81. package/src/ui/HomePageEditorCollectionAction.tsx +84 -0
  82. package/src/ui/MissingReferenceWidget.tsx +35 -0
  83. package/src/ui/NewCollectionButton.tsx +16 -0
  84. package/src/ui/NewCollectionCard.tsx +47 -0
  85. package/src/ui/PropertyAddColumnComponent.tsx +42 -0
  86. package/src/ui/RootCollectionSuggestions.tsx +55 -0
  87. package/src/ui/collection_editor/CollectionDetailsForm.tsx +366 -0
  88. package/src/ui/collection_editor/CollectionEditorDialog.tsx +754 -0
  89. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +206 -0
  90. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +481 -0
  91. package/src/ui/collection_editor/CollectionYupValidation.tsx +7 -0
  92. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +37 -0
  93. package/src/ui/collection_editor/EnumForm.tsx +358 -0
  94. package/src/ui/collection_editor/GetCodeDialog.tsx +120 -0
  95. package/src/ui/collection_editor/PropertyEditView.tsx +558 -0
  96. package/src/ui/collection_editor/PropertyFieldPreview.tsx +204 -0
  97. package/src/ui/collection_editor/PropertySelectItem.tsx +32 -0
  98. package/src/ui/collection_editor/PropertyTree.tsx +233 -0
  99. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +253 -0
  100. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +47 -0
  101. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +37 -0
  102. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +272 -0
  103. package/src/ui/collection_editor/import/clean_import_data.ts +53 -0
  104. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +135 -0
  105. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +36 -0
  106. package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +137 -0
  107. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +87 -0
  108. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +117 -0
  109. package/src/ui/collection_editor/properties/FieldHelperView.tsx +13 -0
  110. package/src/ui/collection_editor/properties/KeyValuePropertyField.tsx +20 -0
  111. package/src/ui/collection_editor/properties/MapPropertyField.tsx +149 -0
  112. package/src/ui/collection_editor/properties/NumberPropertyField.tsx +38 -0
  113. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +182 -0
  114. package/src/ui/collection_editor/properties/RepeatPropertyField.tsx +108 -0
  115. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +194 -0
  116. package/src/ui/collection_editor/properties/StringPropertyField.tsx +79 -0
  117. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +89 -0
  118. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +36 -0
  119. package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +50 -0
  120. package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +50 -0
  121. package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +100 -0
  122. package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +132 -0
  123. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +28 -0
  124. package/src/ui/collection_editor/templates/blog_template.ts +115 -0
  125. package/src/ui/collection_editor/templates/pages_template.ts +188 -0
  126. package/src/ui/collection_editor/templates/products_template.ts +88 -0
  127. package/src/ui/collection_editor/templates/users_template.ts +42 -0
  128. package/src/ui/collection_editor/util.ts +21 -0
  129. package/src/ui/collection_editor/utils/strings.ts +8 -0
  130. package/src/ui/collection_editor/utils/supported_fields.tsx +29 -0
  131. package/src/ui/collection_editor/utils/update_property_for_widget.ts +271 -0
  132. package/src/ui/collection_editor/utils/useTraceUpdate.tsx +23 -0
  133. package/src/useCollectionEditorController.tsx +9 -0
  134. package/src/useCollectionEditorPlugin.tsx +137 -0
  135. package/src/useCollectionsConfigController.tsx +9 -0
  136. package/src/utils/arrays.ts +3 -0
  137. package/src/utils/entities.ts +38 -0
  138. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,253 @@
1
+ import React from "react";
2
+ import {
3
+ DeleteConfirmationDialog,
4
+ EntityCollection,
5
+ EntityCustomView,
6
+ resolveEntityView,
7
+ useFireCMSContext,
8
+ User
9
+ } from "@firecms/core";
10
+ import {
11
+ AddIcon,
12
+ Button,
13
+ Container,
14
+ DeleteIcon,
15
+ IconButton,
16
+ InfoLabel,
17
+ Paper,
18
+ Table,
19
+ TableBody,
20
+ TableCell,
21
+ TableRow,
22
+ Tooltip,
23
+ Typography,
24
+ } from "@firecms/ui";
25
+ import { useFormikContext } from "formik";
26
+ import { CollectionEditorDialog } from "./CollectionEditorDialog";
27
+ import { CollectionsConfigController } from "../../types/config_controller";
28
+ import { PersistedCollection } from "../../types/persisted_collection";
29
+ import { CollectionInference } from "../../types/collection_inference";
30
+ import { EntityCustomViewsSelectDialog } from "./EntityCustomViewsSelectDialog";
31
+
32
+ export function SubcollectionsEditTab({
33
+ collection,
34
+ parentCollection,
35
+ configController,
36
+ collectionInference,
37
+ getUser,
38
+ parentCollectionIds
39
+ }: {
40
+ collection: PersistedCollection,
41
+ parentCollection?: EntityCollection,
42
+ configController: CollectionsConfigController;
43
+ collectionInference?: CollectionInference;
44
+ getUser: (uid: string) => User | null;
45
+ parentCollectionIds?: string[];
46
+ }) {
47
+
48
+ const { entityViews: contextEntityViews } = useFireCMSContext();
49
+
50
+ const [subcollectionToDelete, setSubcollectionToDelete] = React.useState<string | undefined>();
51
+ const [addEntityViewDialogOpen, setAddEntityViewDialogOpen] = React.useState<boolean>(false);
52
+ const [viewToDelete, setViewToDelete] = React.useState<string | undefined>();
53
+
54
+ const [currentDialog, setCurrentDialog] = React.useState<{
55
+ isNewCollection: boolean,
56
+ editedCollectionPath?: string,
57
+ }>();
58
+
59
+ const {
60
+ values,
61
+ setFieldValue,
62
+ } = useFormikContext<EntityCollection>();
63
+
64
+ const subcollections = collection.subcollections ?? [];
65
+ const resolvedEntityViews = values.entityViews?.filter(e => typeof e === "string")
66
+ .map(e => resolveEntityView(e, contextEntityViews))
67
+ .filter(Boolean) as EntityCustomView[] ?? [];
68
+ const hardCodedEntityViews = collection.entityViews?.filter(e => typeof e !== "string") as EntityCustomView[] ?? [];
69
+ const totalEntityViews = resolvedEntityViews.length + hardCodedEntityViews.length;
70
+
71
+ return (
72
+ <div className={"overflow-auto my-auto"}>
73
+ <Container maxWidth={"2xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
74
+ <div className={"flex flex-col gap-16"}>
75
+
76
+ <div className={"flex-grow flex flex-col gap-4 items-start"}>
77
+ <Typography variant={"h5"}>
78
+ Subcollections of {values.name}
79
+ </Typography>
80
+
81
+ {subcollections && subcollections.length > 0 &&
82
+ <Paper className={"flex flex-col gap-4 p-2 w-full"}>
83
+ <Table>
84
+ <TableBody>
85
+ {subcollections.map((subcollection) => (
86
+ <TableRow key={subcollection.path}
87
+ onClick={() => setCurrentDialog({
88
+ isNewCollection: false,
89
+ editedCollectionPath: subcollection.path,
90
+ })}>
91
+ <TableCell
92
+ align="left">
93
+ <Typography variant={"subtitle2"} className={"flex-grow"}>
94
+ {subcollection.name}
95
+ </Typography>
96
+ </TableCell>
97
+ <TableCell
98
+ align="right">
99
+ <Tooltip title={"Remove"}>
100
+ <IconButton size="small"
101
+ onClick={(e) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ setSubcollectionToDelete(subcollection.path);
105
+ }}
106
+ color="inherit">
107
+ <DeleteIcon size={"small"}/>
108
+ </IconButton>
109
+ </Tooltip>
110
+ </TableCell>
111
+ </TableRow>
112
+ ))}
113
+ </TableBody>
114
+ </Table>
115
+ </Paper>}
116
+
117
+ <Button
118
+ onClick={() => {
119
+ setCurrentDialog({
120
+ isNewCollection: true
121
+ });
122
+ }}
123
+ variant={"outlined"}
124
+ startIcon={<AddIcon/>}>
125
+ Add subcollection
126
+ </Button>
127
+ </div>
128
+
129
+ <div className={"flex-grow flex flex-col gap-4 items-start"}>
130
+ <Typography variant={"h5"}>
131
+ Custom views
132
+ </Typography>
133
+
134
+ {totalEntityViews > 0 && <>
135
+ <Paper className={"flex flex-col gap-4 p-2 w-full"}>
136
+ <Table>
137
+ <TableBody>
138
+ {resolvedEntityViews.map((view) => (
139
+ <TableRow key={view.key}>
140
+ <TableCell
141
+ align="left">
142
+ <Typography variant={"subtitle2"} className={"flex-grow"}>
143
+ {view.name}
144
+ </Typography>
145
+ </TableCell>
146
+ <TableCell
147
+ align="right">
148
+ <Tooltip title={"Remove"}>
149
+ <IconButton size="small"
150
+ onClick={(e) => {
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ setViewToDelete(view.key);
154
+ }}
155
+ color="inherit">
156
+ <DeleteIcon size={"small"}/>
157
+ </IconButton>
158
+ </Tooltip>
159
+ </TableCell>
160
+ </TableRow>
161
+ ))}
162
+ {hardCodedEntityViews.map((view) => (
163
+ <TableRow key={view.key}>
164
+ <TableCell
165
+ align="left">
166
+ <Typography variant={"subtitle2"} className={"flex-grow"}>
167
+ {view.name}
168
+ </Typography>
169
+ <Typography variant={"caption"} className={"flex-grow"}>
170
+ This view is defined in code with
171
+ key <code>{view.key}</code>
172
+ </Typography>
173
+ </TableCell>
174
+ </TableRow>
175
+ ))}
176
+ </TableBody>
177
+ </Table>
178
+ </Paper>
179
+
180
+ </>}
181
+
182
+ {totalEntityViews === 0 &&
183
+ <InfoLabel>
184
+ <b>COMING SOON</b> Define your own custom views by uploading it with the CLI
185
+ </InfoLabel>
186
+ }
187
+
188
+ <Button
189
+ onClick={() => {
190
+ setAddEntityViewDialogOpen(true);
191
+ }}
192
+ variant={"outlined"}
193
+ startIcon={<AddIcon/>}>
194
+ Add custom entity view
195
+ </Button>
196
+
197
+ </div>
198
+
199
+ </div>
200
+ </Container>
201
+
202
+ <div style={{ height: "52px" }}/>
203
+
204
+ {subcollectionToDelete &&
205
+ <DeleteConfirmationDialog open={Boolean(subcollectionToDelete)}
206
+ onAccept={() => {
207
+ configController.deleteCollection({
208
+ path: subcollectionToDelete,
209
+ parentCollectionIds: [...(parentCollectionIds ?? []), collection.path]
210
+ });
211
+ setSubcollectionToDelete(undefined);
212
+ }}
213
+ onCancel={() => setSubcollectionToDelete(undefined)}
214
+ title={<>Delete this subcollection?</>}
215
+ body={<> This will <b>not
216
+ delete any data</b>, only
217
+ the collection in the CMS</>}/>}
218
+ {viewToDelete &&
219
+ <DeleteConfirmationDialog open={Boolean(viewToDelete)}
220
+ onAccept={() => {
221
+ setFieldValue("entityViews", values.entityViews?.filter(e => e !== viewToDelete));
222
+ setViewToDelete(undefined);
223
+ }}
224
+ onCancel={() => setViewToDelete(undefined)}
225
+ title={<>Remove this view?</>}
226
+ body={<>This will <b>not
227
+ delete any data</b>, only
228
+ the view in the CMS</>}/>}
229
+
230
+ <CollectionEditorDialog
231
+ open={Boolean(currentDialog)}
232
+ configController={configController}
233
+ parentCollection={collection}
234
+ collectionInference={collectionInference}
235
+ parentCollectionIds={[...parentCollectionIds ?? [], values.id]}
236
+ isNewCollection={false}
237
+ {...currentDialog}
238
+ getUser={getUser}
239
+ handleClose={() => {
240
+ setCurrentDialog(undefined);
241
+ }}/>
242
+
243
+ <EntityCustomViewsSelectDialog
244
+ open={addEntityViewDialogOpen}
245
+ onClose={(selectedViewKey) => {
246
+ if (selectedViewKey) {
247
+ setFieldValue("entityViews", [...(values.entityViews ?? []), selectedViewKey]);
248
+ }
249
+ setAddEntityViewDialogOpen(false);
250
+ }}/>
251
+ </div>
252
+ );
253
+ }
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { Button, Dialog, DialogActions, DialogContent, Typography } from "@firecms/ui";
3
+
4
+ export interface UnsavedChangesDialogProps {
5
+ open: boolean;
6
+ body?: React.ReactNode;
7
+ title?: string;
8
+ handleOk: () => void;
9
+ handleCancel: () => void;
10
+ }
11
+
12
+ export function UnsavedChangesDialog({
13
+ open,
14
+ handleOk,
15
+ handleCancel,
16
+ body,
17
+ title
18
+ }: UnsavedChangesDialogProps) {
19
+
20
+ return (
21
+ <Dialog
22
+ open={open}
23
+ onOpenChange={(open) => open ? handleCancel() : handleOk()}
24
+ aria-labelledby="alert-dialog-title"
25
+ aria-describedby="alert-dialog-description"
26
+ >
27
+ <DialogContent>
28
+ <Typography variant={"h6"}>
29
+ {title ?? "Unsaved changes"}
30
+ </Typography>
31
+
32
+ {body && <Typography>
33
+ {body}
34
+ </Typography>}
35
+ <Typography>
36
+ Are you sure?
37
+ </Typography>
38
+
39
+ </DialogContent>
40
+
41
+ <DialogActions>
42
+ <Button variant="text" onClick={handleCancel} autoFocus> Cancel </Button>
43
+ <Button onClick={handleOk}> Ok </Button>
44
+ </DialogActions>
45
+ </Dialog>
46
+ );
47
+ }
@@ -0,0 +1,37 @@
1
+ import { convertDataToEntity, getPropertiesMapping, ImportConfig } from "@firecms/data_import_export";
2
+ import { EntityCollectionTable, Properties, useSelectionController } from "@firecms/core";
3
+ import { useEffect } from "react";
4
+ import { Typography } from "@firecms/ui";
5
+
6
+ export function CollectionEditorImportDataPreview({ importConfig, properties, propertiesOrder }: {
7
+ importConfig: ImportConfig,
8
+ properties: Properties,
9
+ propertiesOrder: string[]
10
+ }) {
11
+
12
+ useEffect(() => {
13
+ const propertiesMapping = getPropertiesMapping(importConfig.originProperties, properties);
14
+ const mappedData = importConfig.importData.map(d => convertDataToEntity(d, importConfig.idColumn, importConfig.headersMapping, properties, propertiesMapping, "TEMP_PATH"));
15
+ importConfig.setEntities(mappedData);
16
+ }, []);
17
+
18
+ const selectionController = useSelectionController();
19
+
20
+ return <EntityCollectionTable
21
+ title={<div>
22
+ <Typography variant={"subtitle2"}>Imported data preview</Typography>
23
+ <Typography variant={"caption"}>Entities with the same id will be overwritten</Typography>
24
+ </div>}
25
+ tableController={{
26
+ data: importConfig.entities,
27
+ dataLoading: false,
28
+ noMoreToLoad: false
29
+ }}
30
+ endAdornment={<div className={"h-12"}/>}
31
+ filterable={false}
32
+ sortable={false}
33
+ selectionController={selectionController}
34
+ displayedColumnIds={propertiesOrder.map(p => ({ key: p, disabled: false }))}
35
+ properties={properties}/>
36
+
37
+ }
@@ -0,0 +1,272 @@
1
+ import {
2
+ DataNewPropertiesMapping,
3
+ getInferenceType,
4
+ ImportConfig,
5
+ ImportNewPropertyFieldPreview
6
+ } from "@firecms/data_import_export";
7
+ import { getIn, useFormikContext } from "formik";
8
+
9
+ import {
10
+ FieldConfigBadge,
11
+ getFieldConfig,
12
+ getFieldId,
13
+ Properties,
14
+ Property,
15
+ PropertyConfig,
16
+ } from "@firecms/core";
17
+ import {
18
+ Container,
19
+ Select,
20
+ Tooltip,
21
+ Typography
22
+ } from "@firecms/ui";
23
+ import React, { useState } from "react";
24
+ import { OnPropertyChangedParams, PropertyFormDialog, PropertyWithId } from "../PropertyEditView";
25
+ import { getFullId, idToPropertiesPath, namespaceToPropertiesOrderPath } from "../util";
26
+ import { PersistedCollection } from "../../../types/persisted_collection";
27
+ import { updatePropertyFromWidget } from "../utils/update_property_for_widget";
28
+ import { PropertySelectItem } from "../PropertySelectItem";
29
+ import { supportedFields } from "../utils/supported_fields";
30
+ import { buildPropertyFromData } from "@firecms/schema_inference";
31
+
32
+ export function CollectionEditorImportMapping({
33
+ importConfig,
34
+ propertyConfigs,
35
+ collectionEditable
36
+ }:
37
+ {
38
+ importConfig: ImportConfig,
39
+ propertyConfigs: Record<string, PropertyConfig>,
40
+ collectionEditable: boolean
41
+ }) {
42
+
43
+ const {
44
+ setFieldValue,
45
+ setFieldTouched,
46
+ values
47
+ } = useFormikContext<PersistedCollection>();
48
+ const [selectedProperty, setSelectedProperty] = useState<PropertyWithId | undefined>(undefined);
49
+
50
+ const currentPropertiesOrderRef = React.useRef<{
51
+ [key: string]: string[]
52
+ }>(values.propertiesOrder ? { "": values.propertiesOrder } : {});
53
+
54
+ const propertyKey = selectedProperty ? selectedProperty.id : undefined;
55
+ const property = selectedProperty || undefined;
56
+
57
+ const onPropertyChanged = ({
58
+ id,
59
+ property,
60
+ previousId,
61
+ namespace
62
+ }: OnPropertyChangedParams) => {
63
+
64
+ const fullId = id ? getFullId(id, namespace) : undefined;
65
+ const propertyPath = fullId ? idToPropertiesPath(fullId) : undefined;
66
+
67
+ // setSelectedProperty(property);
68
+ const getCurrentPropertiesOrder = (namespace?: string) => {
69
+ if (!namespace) return currentPropertiesOrderRef.current[""];
70
+ return currentPropertiesOrderRef.current[namespace] ?? getIn(values, namespaceToPropertiesOrderPath(namespace));
71
+ }
72
+
73
+ const updatePropertiesOrder = (newPropertiesOrder: string[], namespace?: string) => {
74
+ const propertiesOrderPath = namespaceToPropertiesOrderPath(namespace);
75
+
76
+ setFieldValue(propertiesOrderPath, newPropertiesOrder, false);
77
+ currentPropertiesOrderRef.current[namespace ?? ""] = newPropertiesOrder;
78
+
79
+ };
80
+
81
+ // If the id has changed we need to a little cleanup
82
+ if (previousId && previousId !== id) {
83
+ const previousFullId = getFullId(previousId, namespace);
84
+ const previousPropertyPath = idToPropertiesPath(previousFullId);
85
+
86
+ const currentPropertiesOrder = getCurrentPropertiesOrder(namespace);
87
+
88
+ // replace previousId with id in propertiesOrder
89
+ const newPropertiesOrder = currentPropertiesOrder
90
+ .map((p) => p === previousId ? id : p)
91
+ .filter((p) => p !== undefined) as string[];
92
+ updatePropertiesOrder(newPropertiesOrder, namespace);
93
+
94
+ // replace previousId with id in headersMapping
95
+ const newHeadersMapping = { ...importConfig.headersMapping };
96
+ Object.keys(newHeadersMapping).forEach((key) => {
97
+ if (newHeadersMapping[key] === previousId) {
98
+ newHeadersMapping[key] = id ?? "";
99
+ }
100
+ });
101
+ importConfig.setHeadersMapping(newHeadersMapping);
102
+
103
+ // if (id) {
104
+ // setSelectedPropertyIndex(newPropertiesOrder.indexOf(id));
105
+ // setSelectedPropertyKey(id);
106
+ // }
107
+ setFieldValue(previousPropertyPath, undefined, false);
108
+ setFieldTouched(previousPropertyPath, false, false);
109
+ }
110
+
111
+ if (propertyPath) {
112
+ setFieldValue(propertyPath, property, false);
113
+ setFieldTouched(propertyPath, true, false);
114
+ }
115
+ };
116
+ const onPropertyTypeChanged = async ({
117
+ id,
118
+ importKey,
119
+ property,
120
+ namespace
121
+ }: OnPropertyChangedParams & {
122
+ importKey: string
123
+ }) => {
124
+
125
+ const fullId = id ? getFullId(id, namespace) : undefined;
126
+ const propertyPath = fullId ? idToPropertiesPath(fullId) : undefined;
127
+
128
+ // we try to infer the rest of the properties of a property, from the type and the data
129
+ const propertyData = importConfig.importData.map((d) => getIn(d, importKey));
130
+ const inferredNewProperty = {
131
+ ...buildPropertyFromData(propertyData, property, getInferenceType),
132
+ editable: true
133
+ };
134
+
135
+ if (propertyPath) {
136
+ if (inferredNewProperty) {
137
+ setFieldValue(propertyPath, inferredNewProperty, false);
138
+ } else {
139
+ setFieldValue(propertyPath, property, false);
140
+ }
141
+ setFieldTouched(propertyPath, true, false);
142
+ }
143
+ };
144
+
145
+ return (
146
+
147
+ <div className={"overflow-auto my-auto bg-gray-50 dark:bg-gray-900"}>
148
+ <Container maxWidth={"6xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
149
+
150
+ <Typography variant="h6" className={"mt-4"}>Data property mapping</Typography>
151
+
152
+ <DataNewPropertiesMapping headersMapping={importConfig.headersMapping}
153
+ idColumn={importConfig.idColumn}
154
+ originProperties={importConfig.originProperties}
155
+ destinationProperties={values.properties as Properties}
156
+ onIdPropertyChanged={(value) => importConfig.setIdColumn(value)}
157
+ buildPropertyView={({
158
+ property,
159
+ propertyKey,
160
+ importKey
161
+ }) => {
162
+ return <ImportNewPropertyFieldPreview
163
+ property={property}
164
+ propertyKey={propertyKey}
165
+ onPropertyNameChanged={(propertyKey: string, value: string) => setFieldValue(`properties.${propertyKey}.name`, value, false)}
166
+ onEditClick={() => {
167
+ if (!propertyKey || !property) return;
168
+ setSelectedProperty({
169
+ ...property,
170
+ id: propertyKey,
171
+ editable: true
172
+ });
173
+ }}
174
+ propertyTypeView={<PropertySelect property={property}
175
+ disabled={false}
176
+ onPropertyChanged={(props) => onPropertyTypeChanged({
177
+ ...props,
178
+ importKey
179
+ })}
180
+ propertyKey={propertyKey}
181
+ propertyConfigs={propertyConfigs}/>}
182
+ />;
183
+ }}/>
184
+ </Container>
185
+
186
+ <PropertyFormDialog
187
+ open={selectedProperty !== undefined}
188
+ propertyKey={propertyKey}
189
+ property={property}
190
+ inArray={false}
191
+ autoUpdateId={false}
192
+ onPropertyChanged={onPropertyChanged}
193
+ allowDataInference={false}
194
+ collectionEditable={collectionEditable}
195
+ onOkClicked={() => {
196
+ setSelectedProperty(undefined);
197
+ }}
198
+ onCancel={() => {
199
+ setSelectedProperty(undefined);
200
+ }}
201
+ autoOpenTypeSelect={false}
202
+ existingProperty={false}
203
+ propertyConfigs={propertyConfigs}/>
204
+
205
+ <div style={{ height: "52px" }}/>
206
+ </div>
207
+ );
208
+
209
+ }
210
+
211
+ function PropertySelect({
212
+ property,
213
+ onPropertyChanged,
214
+ propertyKey,
215
+ propertyConfigs,
216
+ disabled
217
+ }: {
218
+ property: Property | null,
219
+ propertyKey: string | null,
220
+ onPropertyChanged: ({
221
+ id,
222
+ property,
223
+ previousId,
224
+ namespace
225
+ }: OnPropertyChangedParams) => void,
226
+ propertyConfigs: Record<string, PropertyConfig>,
227
+ disabled?: boolean
228
+ }) {
229
+
230
+ const fieldId = property ? getFieldId(property) : null;
231
+ const widget = property ? getFieldConfig(property, propertyConfigs) : null;
232
+
233
+ const [selectOpen, setSelectOpen] = useState(false);
234
+
235
+ return <Tooltip title={property && widget ? `${widget?.name} - ${property.dataType}` : undefined}
236
+ open={selectOpen ? false : undefined}>
237
+ <Select
238
+ open={selectOpen}
239
+ onOpenChange={setSelectOpen}
240
+ invisible={true}
241
+ className={"w-full"}
242
+ disabled={disabled}
243
+ error={!widget}
244
+ value={fieldId ?? ""}
245
+ placeholder={"Select a property widget"}
246
+ position={"item-aligned"}
247
+ renderValue={(value) => {
248
+ if (!widget) return null;
249
+ return <FieldConfigBadge propertyConfig={widget}/>
250
+ }}
251
+ onValueChange={(newSelectedWidgetId) => {
252
+ const newProperty = updatePropertyFromWidget(property, newSelectedWidgetId, propertyConfigs)
253
+ if (!propertyKey) return;
254
+ onPropertyChanged({
255
+ id: propertyKey,
256
+ property: newProperty,
257
+ previousId: propertyKey,
258
+ namespace: undefined
259
+ });
260
+ }}>
261
+ {Object.entries(supportedFields).map(([key, widget]) => {
262
+ return <PropertySelectItem
263
+ key={key}
264
+ value={key}
265
+ optionDisabled={false}
266
+ propertyConfig={widget}
267
+ existing={false}/>;
268
+ })
269
+ }
270
+ </Select>
271
+ </Tooltip>;
272
+ }
@@ -0,0 +1,53 @@
1
+ import { Properties, slugify } from "@firecms/core";
2
+ import { ImportConfig } from "@firecms/data_import_export";
3
+
4
+ export function cleanPropertiesFromImport(properties: Properties, parentSlug = ""): {
5
+ headersMapping: ImportConfig["headersMapping"],
6
+ properties: Properties,
7
+ idColumn?: ImportConfig["idColumn"],
8
+ } {
9
+
10
+ const result = Object.keys(properties).reduce((acc, key) => {
11
+ const property = properties[key];
12
+ const slug = slugify(key);
13
+ const fullSlug = parentSlug ? `${parentSlug}.${slug}` : slug;
14
+
15
+ if (property.dataType === "map" && property.properties) {
16
+ const slugifiedResult = cleanPropertiesFromImport(property.properties as Properties, fullSlug);
17
+ return {
18
+ headersMapping: { ...acc.headersMapping, [key]: fullSlug },
19
+ properties: {
20
+ ...acc.properties,
21
+ [slug]: {
22
+ ...property,
23
+ properties: slugifiedResult.properties,
24
+ propertiesOrder: Object.keys(slugifiedResult.properties)
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ const updatedProperties = {
31
+ ...acc.properties,
32
+ [slug]: property
33
+ } as Properties;
34
+
35
+ const headersMapping = { ...acc.headersMapping, [key]: fullSlug } as Record<string, string>;
36
+
37
+ return {
38
+ headersMapping,
39
+ properties: updatedProperties,
40
+ }
41
+ }, { headersMapping: {}, properties: {} });
42
+
43
+ const firstKey = Object.keys(result.headersMapping)?.[0];
44
+ let idColumn: string | undefined;
45
+ if (firstKey?.includes("id") || firstKey?.includes("key")) {
46
+ idColumn = firstKey;
47
+ }
48
+
49
+ return {
50
+ ...result,
51
+ idColumn
52
+ };
53
+ }