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