@firecms/collection_editor 3.0.0-alpha.8 → 3.0.0-alpha.81
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/dist/ConfigControllerProvider.d.ts +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.es.js +5257 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +4 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/collection_editor_controller.d.ts +35 -0
- package/dist/types/collection_inference.d.ts +2 -0
- package/dist/types/config_controller.d.ts +51 -0
- package/dist/types/config_permissions.d.ts +19 -0
- package/dist/types/persisted_collection.d.ts +6 -0
- package/dist/ui/CollectionViewHeaderAction.d.ts +10 -0
- package/dist/ui/EditorCollectionAction.d.ts +2 -0
- package/dist/ui/HomePageEditorCollectionAction.d.ts +2 -0
- package/dist/ui/MissingReferenceWidget.d.ts +3 -0
- package/dist/ui/NewCollectionButton.d.ts +1 -0
- package/dist/ui/NewCollectionCard.d.ts +2 -0
- package/dist/ui/PropertyAddColumnComponent.d.ts +6 -0
- package/dist/ui/RootCollectionSuggestions.d.ts +1 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +10 -0
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +38 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +15 -0
- package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionYupValidation.d.ts +14 -0
- package/dist/ui/collection_editor/EntityCustomViewsSelectDialog.d.ts +4 -0
- package/dist/ui/collection_editor/EnumForm.d.ts +13 -0
- package/dist/ui/collection_editor/GetCodeDialog.d.ts +5 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +40 -0
- package/dist/ui/collection_editor/PropertyFieldPreview.d.ts +15 -0
- package/dist/ui/collection_editor/PropertySelectItem.d.ts +8 -0
- package/dist/ui/collection_editor/PropertyTree.d.ts +32 -0
- package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +12 -0
- package/dist/ui/collection_editor/UnsavedChangesDialog.d.ts +9 -0
- package/dist/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +7 -0
- package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +7 -0
- package/dist/ui/collection_editor/import/clean_import_data.d.ts +7 -0
- package/dist/ui/collection_editor/properties/BlockPropertyField.d.ts +8 -0
- package/dist/ui/collection_editor/properties/BooleanPropertyField.d.ts +3 -0
- package/dist/ui/collection_editor/properties/CommonPropertyFields.d.ts +11 -0
- package/dist/ui/collection_editor/properties/DateTimePropertyField.d.ts +3 -0
- package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +8 -0
- package/dist/ui/collection_editor/properties/FieldHelperView.d.ts +4 -0
- package/dist/ui/collection_editor/properties/KeyValuePropertyField.d.ts +3 -0
- package/dist/ui/collection_editor/properties/MapPropertyField.d.ts +8 -0
- package/dist/ui/collection_editor/properties/NumberPropertyField.d.ts +3 -0
- package/dist/ui/collection_editor/properties/ReferencePropertyField.d.ts +13 -0
- package/dist/ui/collection_editor/properties/RepeatPropertyField.d.ts +10 -0
- package/dist/ui/collection_editor/properties/StoragePropertyField.d.ts +5 -0
- package/dist/ui/collection_editor/properties/StringPropertyField.d.ts +5 -0
- package/dist/ui/collection_editor/properties/UrlPropertyField.d.ts +4 -0
- package/dist/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.d.ts +3 -0
- package/dist/ui/collection_editor/properties/validation/ArrayPropertyValidation.d.ts +5 -0
- package/dist/ui/collection_editor/properties/validation/GeneralPropertyValidation.d.ts +4 -0
- package/dist/ui/collection_editor/properties/validation/NumberPropertyValidation.d.ts +3 -0
- package/dist/ui/collection_editor/properties/validation/StringPropertyValidation.d.ts +11 -0
- package/dist/ui/collection_editor/properties/validation/ValidationPanel.d.ts +2 -0
- package/dist/ui/collection_editor/templates/blog_template.d.ts +2 -0
- package/dist/ui/collection_editor/templates/pages_template.d.ts +2 -0
- package/dist/ui/collection_editor/templates/products_template.d.ts +2 -0
- package/dist/ui/collection_editor/templates/users_template.d.ts +2 -0
- package/dist/ui/collection_editor/util.d.ts +4 -0
- package/dist/ui/collection_editor/utils/strings.d.ts +1 -0
- package/dist/ui/collection_editor/utils/supported_fields.d.ts +3 -0
- package/dist/ui/collection_editor/utils/update_property_for_widget.d.ts +2 -0
- package/dist/ui/collection_editor/utils/useTraceUpdate.d.ts +1 -0
- package/dist/useCollectionEditorController.d.ts +6 -0
- package/dist/useCollectionEditorPlugin.d.ts +46 -0
- package/dist/useCollectionsConfigController.d.ts +6 -0
- package/dist/utils/arrays.d.ts +1 -0
- package/dist/utils/entities.d.ts +3 -0
- package/package.json +24 -27
- package/src/ConfigControllerProvider.tsx +330 -0
- package/src/index.ts +35 -0
- package/src/types/collection_editor_controller.tsx +42 -0
- package/src/types/collection_inference.ts +3 -0
- package/src/types/config_controller.tsx +60 -0
- package/src/types/config_permissions.ts +20 -0
- package/src/types/persisted_collection.ts +9 -0
- package/src/ui/CollectionViewHeaderAction.tsx +43 -0
- package/src/ui/EditorCollectionAction.tsx +109 -0
- package/src/ui/HomePageEditorCollectionAction.tsx +84 -0
- package/src/ui/MissingReferenceWidget.tsx +35 -0
- package/src/ui/NewCollectionButton.tsx +16 -0
- package/src/ui/NewCollectionCard.tsx +47 -0
- package/src/ui/PropertyAddColumnComponent.tsx +42 -0
- package/src/ui/RootCollectionSuggestions.tsx +55 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +366 -0
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +754 -0
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +206 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +481 -0
- package/src/ui/collection_editor/CollectionYupValidation.tsx +7 -0
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +37 -0
- package/src/ui/collection_editor/EnumForm.tsx +358 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +120 -0
- package/src/ui/collection_editor/PropertyEditView.tsx +558 -0
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +204 -0
- package/src/ui/collection_editor/PropertySelectItem.tsx +32 -0
- package/src/ui/collection_editor/PropertyTree.tsx +233 -0
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +253 -0
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +47 -0
- package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +37 -0
- package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +272 -0
- package/src/ui/collection_editor/import/clean_import_data.ts +53 -0
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +135 -0
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +36 -0
- package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +137 -0
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +87 -0
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +117 -0
- package/src/ui/collection_editor/properties/FieldHelperView.tsx +13 -0
- package/src/ui/collection_editor/properties/KeyValuePropertyField.tsx +20 -0
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +149 -0
- package/src/ui/collection_editor/properties/NumberPropertyField.tsx +38 -0
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +182 -0
- package/src/ui/collection_editor/properties/RepeatPropertyField.tsx +108 -0
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +194 -0
- package/src/ui/collection_editor/properties/StringPropertyField.tsx +79 -0
- package/src/ui/collection_editor/properties/UrlPropertyField.tsx +89 -0
- package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +36 -0
- package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +50 -0
- package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +50 -0
- package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +100 -0
- package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +132 -0
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +28 -0
- package/src/ui/collection_editor/templates/blog_template.ts +115 -0
- package/src/ui/collection_editor/templates/pages_template.ts +188 -0
- package/src/ui/collection_editor/templates/products_template.ts +88 -0
- package/src/ui/collection_editor/templates/users_template.ts +42 -0
- package/src/ui/collection_editor/util.ts +21 -0
- package/src/ui/collection_editor/utils/strings.ts +8 -0
- package/src/ui/collection_editor/utils/supported_fields.tsx +29 -0
- package/src/ui/collection_editor/utils/update_property_for_widget.ts +271 -0
- package/src/ui/collection_editor/utils/useTraceUpdate.tsx +23 -0
- package/src/useCollectionEditorController.tsx +9 -0
- package/src/useCollectionEditorPlugin.tsx +137 -0
- package/src/useCollectionsConfigController.tsx +9 -0
- package/src/utils/arrays.ts +3 -0
- package/src/utils/entities.ts +38 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { EntityCollection, unslugify, } from "@firecms/core";
|
|
3
|
+
import { Button, Card, Chip, CircularProgress, cn, Container, Icon, Tooltip, Typography, } from "@firecms/ui";
|
|
4
|
+
import { useFormikContext } from "formik";
|
|
5
|
+
|
|
6
|
+
import { productsCollectionTemplate } from "./templates/products_template";
|
|
7
|
+
import { blogCollectionTemplate } from "./templates/blog_template";
|
|
8
|
+
import { usersCollectionTemplate } from "./templates/users_template";
|
|
9
|
+
import { ImportFileUpload } from "@firecms/data_import_export";
|
|
10
|
+
import { pagesCollectionTemplate } from "./templates/pages_template";
|
|
11
|
+
|
|
12
|
+
export function CollectionEditorWelcomeView({
|
|
13
|
+
path,
|
|
14
|
+
pathSuggestions,
|
|
15
|
+
parentCollection,
|
|
16
|
+
onContinue,
|
|
17
|
+
collections
|
|
18
|
+
}: {
|
|
19
|
+
path: string;
|
|
20
|
+
pathSuggestions?: (path: string) => Promise<string[]>;
|
|
21
|
+
parentCollection?: EntityCollection;
|
|
22
|
+
onContinue: (importData?: object[]) => void;
|
|
23
|
+
collections?: EntityCollection[];
|
|
24
|
+
}) {
|
|
25
|
+
|
|
26
|
+
const [loadingPathSuggestions, setLoadingPathSuggestions] = useState(false);
|
|
27
|
+
const [filteredPathSuggestions, setFilteredPathSuggestions] = useState<string[] | undefined>();
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (pathSuggestions && collections) {
|
|
30
|
+
setLoadingPathSuggestions(true);
|
|
31
|
+
pathSuggestions(path)
|
|
32
|
+
.then(suggestions => {
|
|
33
|
+
const filteredSuggestions = suggestions.filter(s => !collections.find(c => c.path.trim().toLowerCase() === s.trim().toLowerCase()));
|
|
34
|
+
setFilteredPathSuggestions(filteredSuggestions);
|
|
35
|
+
})
|
|
36
|
+
.finally(() => setLoadingPathSuggestions(false));
|
|
37
|
+
}
|
|
38
|
+
}, [collections, path, pathSuggestions]);
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
values,
|
|
42
|
+
setFieldValue,
|
|
43
|
+
setValues,
|
|
44
|
+
handleChange,
|
|
45
|
+
touched,
|
|
46
|
+
errors,
|
|
47
|
+
setFieldTouched,
|
|
48
|
+
isSubmitting,
|
|
49
|
+
submitCount
|
|
50
|
+
} = useFormikContext<EntityCollection>();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={"overflow-auto my-auto"}>
|
|
54
|
+
<Container maxWidth={"4xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
className="flex flex-row py-2 pt-3 items-center">
|
|
58
|
+
<Typography variant={"h4"} className={"flex-grow"}>
|
|
59
|
+
New collection
|
|
60
|
+
</Typography>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{parentCollection && <Chip colorScheme={"tealDarker"}>
|
|
64
|
+
<Typography variant={"caption"}>
|
|
65
|
+
This is a subcollection of <b>{parentCollection.name}</b>
|
|
66
|
+
</Typography>
|
|
67
|
+
</Chip>}
|
|
68
|
+
|
|
69
|
+
<div className={"my-2"}>
|
|
70
|
+
<Typography variant={"caption"}
|
|
71
|
+
color={"secondary"}>
|
|
72
|
+
● Use one of the existing paths in your database:
|
|
73
|
+
</Typography>
|
|
74
|
+
<div className={"flex flex-wrap gap-x-2 gap-y-1 items-center my-2 min-h-7"}>
|
|
75
|
+
|
|
76
|
+
{loadingPathSuggestions && !filteredPathSuggestions && <CircularProgress size={"small"}/>}
|
|
77
|
+
|
|
78
|
+
{filteredPathSuggestions?.map((suggestion, index) => (
|
|
79
|
+
<Chip key={suggestion}
|
|
80
|
+
colorScheme={"cyanLighter"}
|
|
81
|
+
onClick={() => {
|
|
82
|
+
setFieldValue("name", unslugify(suggestion));
|
|
83
|
+
setFieldValue("id", suggestion);
|
|
84
|
+
setFieldValue("path", suggestion);
|
|
85
|
+
setFieldValue("properties", undefined);
|
|
86
|
+
onContinue();
|
|
87
|
+
}}
|
|
88
|
+
size="small">
|
|
89
|
+
{suggestion}
|
|
90
|
+
</Chip>
|
|
91
|
+
))}
|
|
92
|
+
|
|
93
|
+
{!loadingPathSuggestions && (filteredPathSuggestions ?? [])?.length === 0 &&
|
|
94
|
+
<Typography variant={"caption"}>
|
|
95
|
+
No suggestions
|
|
96
|
+
</Typography>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className={"my-2"}>
|
|
104
|
+
<Typography variant={"caption"}
|
|
105
|
+
color={"secondary"}>
|
|
106
|
+
● Select a template:
|
|
107
|
+
</Typography>
|
|
108
|
+
|
|
109
|
+
<div className={"flex gap-4"}>
|
|
110
|
+
<TemplateButton title={"Products"}
|
|
111
|
+
subtitle={"A collection of products with images, prices and stock"}
|
|
112
|
+
icon={<Icon size={"small"} iconKey={productsCollectionTemplate.icon!}/>}
|
|
113
|
+
onClick={() => {
|
|
114
|
+
setValues(productsCollectionTemplate);
|
|
115
|
+
onContinue();
|
|
116
|
+
}}/>
|
|
117
|
+
<TemplateButton title={"Users"}
|
|
118
|
+
subtitle={"A collection of users with emails, names and roles"}
|
|
119
|
+
icon={<Icon size={"small"} iconKey={usersCollectionTemplate.icon!}/>}
|
|
120
|
+
onClick={() => {
|
|
121
|
+
setValues(usersCollectionTemplate);
|
|
122
|
+
onContinue();
|
|
123
|
+
}}/>
|
|
124
|
+
<TemplateButton title={"Blog posts"}
|
|
125
|
+
subtitle={"A collection of blog posts with images, authors and complex content"}
|
|
126
|
+
icon={<Icon size={"small"} iconKey={blogCollectionTemplate.icon!}/>}
|
|
127
|
+
onClick={() => {
|
|
128
|
+
setValues(blogCollectionTemplate);
|
|
129
|
+
onContinue();
|
|
130
|
+
}}/>
|
|
131
|
+
<TemplateButton title={"Pages"}
|
|
132
|
+
subtitle={"A collection of pages with images, authors and complex content"}
|
|
133
|
+
icon={<Icon size={"small"} iconKey={pagesCollectionTemplate.icon!}/>}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setValues(pagesCollectionTemplate);
|
|
136
|
+
onContinue();
|
|
137
|
+
}}/>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{!parentCollection && <div>
|
|
143
|
+
|
|
144
|
+
<Typography variant={"caption"}
|
|
145
|
+
color={"secondary"}
|
|
146
|
+
className={"mb-2"}>
|
|
147
|
+
● Create a collection from a file (csv, json, xls, xslx...)
|
|
148
|
+
</Typography>
|
|
149
|
+
|
|
150
|
+
<ImportFileUpload onDataAdded={(data) => onContinue(data)}/>
|
|
151
|
+
|
|
152
|
+
</div>}
|
|
153
|
+
|
|
154
|
+
<div>
|
|
155
|
+
|
|
156
|
+
<Button variant={"text"} onClick={() => onContinue()} className={"my-2"}>
|
|
157
|
+
Continue from scratch
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/*<div style={{ height: "52px" }}/>*/}
|
|
162
|
+
|
|
163
|
+
</Container>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function TemplateButton({
|
|
169
|
+
title,
|
|
170
|
+
subtitle,
|
|
171
|
+
icon,
|
|
172
|
+
onClick
|
|
173
|
+
}: {
|
|
174
|
+
title: string,
|
|
175
|
+
icon: React.ReactNode,
|
|
176
|
+
subtitle: string,
|
|
177
|
+
onClick?: () => void
|
|
178
|
+
}) {
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Tooltip title={subtitle}>
|
|
182
|
+
<Card
|
|
183
|
+
onClick={onClick}
|
|
184
|
+
className={cn(
|
|
185
|
+
"my-2 rounded-md border mx-0 p-6 px-4 focus:outline-none transition ease-in-out duration-150 flex flex-row gap-4 items-center",
|
|
186
|
+
"text-gray-700 dark:text-gray-300",
|
|
187
|
+
"hover:border-primary-dark hover:text-primary-dark dark:hover:text-primary focus:ring-primary hover:ring-1 hover:ring-primary",
|
|
188
|
+
"border-gray-400 dark:border-gray-600 "
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{icon}
|
|
192
|
+
<div className={"flex flex-col items-start"}>
|
|
193
|
+
|
|
194
|
+
<Typography variant={"subtitle1"}>
|
|
195
|
+
{title}
|
|
196
|
+
</Typography>
|
|
197
|
+
{/*<Typography>*/}
|
|
198
|
+
{/* {subtitle}*/}
|
|
199
|
+
{/*</Typography>*/}
|
|
200
|
+
|
|
201
|
+
</div>
|
|
202
|
+
</Card>
|
|
203
|
+
</Tooltip>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Field, FormikErrors, getIn, useFormikContext } from "formik";
|
|
4
|
+
import {
|
|
5
|
+
EntityCollection,
|
|
6
|
+
ErrorBoundary,
|
|
7
|
+
isPropertyBuilder,
|
|
8
|
+
makePropertiesEditable,
|
|
9
|
+
Properties,
|
|
10
|
+
Property,
|
|
11
|
+
PropertyConfig,
|
|
12
|
+
PropertyOrBuilder,
|
|
13
|
+
useLargeLayout,
|
|
14
|
+
User,
|
|
15
|
+
useSnackbarController
|
|
16
|
+
} from "@firecms/core";
|
|
17
|
+
import {
|
|
18
|
+
AddIcon,
|
|
19
|
+
AutoAwesomeIcon,
|
|
20
|
+
Button,
|
|
21
|
+
CircularProgress,
|
|
22
|
+
cn,
|
|
23
|
+
CodeIcon,
|
|
24
|
+
DebouncedTextField,
|
|
25
|
+
defaultBorderMixin,
|
|
26
|
+
IconButton,
|
|
27
|
+
Paper,
|
|
28
|
+
Tooltip,
|
|
29
|
+
Typography,
|
|
30
|
+
} from "@firecms/ui";
|
|
31
|
+
|
|
32
|
+
import { getFullId, idToPropertiesPath, namespaceToPropertiesOrderPath } from "./util";
|
|
33
|
+
import { OnPropertyChangedParams, PropertyForm, PropertyFormDialog } from "./PropertyEditView";
|
|
34
|
+
import { PropertyTree } from "./PropertyTree";
|
|
35
|
+
import { PersistedCollection } from "../../types/persisted_collection";
|
|
36
|
+
import { GetCodeDialog } from "./GetCodeDialog";
|
|
37
|
+
|
|
38
|
+
type CollectionEditorFormProps = {
|
|
39
|
+
showErrors: boolean;
|
|
40
|
+
isNewCollection: boolean;
|
|
41
|
+
propertyErrorsRef?: React.MutableRefObject<any>;
|
|
42
|
+
onPropertyError: (propertyKey: string, namespace: string | undefined, error?: FormikErrors<any>) => void;
|
|
43
|
+
setDirty?: (dirty: boolean) => void;
|
|
44
|
+
reservedGroups?: string[];
|
|
45
|
+
extraIcon: React.ReactNode;
|
|
46
|
+
getUser: (uid: string) => User | null;
|
|
47
|
+
getData?: () => Promise<object[]>;
|
|
48
|
+
doCollectionInference: (collection: PersistedCollection) => Promise<Partial<EntityCollection> | null> | undefined;
|
|
49
|
+
propertyConfigs: Record<string, PropertyConfig>;
|
|
50
|
+
collectionEditable: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function CollectionPropertiesEditorForm({
|
|
54
|
+
showErrors,
|
|
55
|
+
isNewCollection,
|
|
56
|
+
propertyErrorsRef,
|
|
57
|
+
onPropertyError,
|
|
58
|
+
setDirty,
|
|
59
|
+
reservedGroups,
|
|
60
|
+
extraIcon,
|
|
61
|
+
getUser,
|
|
62
|
+
getData,
|
|
63
|
+
doCollectionInference,
|
|
64
|
+
propertyConfigs,
|
|
65
|
+
collectionEditable
|
|
66
|
+
}: CollectionEditorFormProps) {
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
values,
|
|
70
|
+
setFieldValue,
|
|
71
|
+
setFieldError,
|
|
72
|
+
setFieldTouched,
|
|
73
|
+
errors,
|
|
74
|
+
dirty
|
|
75
|
+
} = useFormikContext<PersistedCollection>();
|
|
76
|
+
|
|
77
|
+
const snackbarController = useSnackbarController();
|
|
78
|
+
|
|
79
|
+
const largeLayout = useLargeLayout();
|
|
80
|
+
const asDialog = !largeLayout
|
|
81
|
+
|
|
82
|
+
// index of the selected property within the namespace
|
|
83
|
+
const [selectedPropertyIndex, setSelectedPropertyIndex] = useState<number | undefined>();
|
|
84
|
+
const [selectedPropertyKey, setSelectedPropertyKey] = useState<string | undefined>();
|
|
85
|
+
const [selectedPropertyNamespace, setSelectedPropertyNamespace] = useState<string | undefined>();
|
|
86
|
+
|
|
87
|
+
const selectedPropertyFullId = selectedPropertyKey ? getFullId(selectedPropertyKey, selectedPropertyNamespace) : undefined;
|
|
88
|
+
const selectedProperty = selectedPropertyFullId ? getIn(values.properties, selectedPropertyFullId.replaceAll(".", ".properties.")) : undefined;
|
|
89
|
+
const [codeDialogOpen, setCodeDialogOpen] = useState<boolean>(false);
|
|
90
|
+
|
|
91
|
+
const [inferringProperties, setInferringProperties] = useState<boolean>(false);
|
|
92
|
+
|
|
93
|
+
const [newPropertyDialogOpen, setNewPropertyDialogOpen] = useState<boolean>(false);
|
|
94
|
+
const [inferredPropertyKeys, setInferredPropertyKeys] = useState<string[]>([]);
|
|
95
|
+
|
|
96
|
+
const currentPropertiesOrderRef = React.useRef<{
|
|
97
|
+
[key: string]: string[]
|
|
98
|
+
}>(values.propertiesOrder ? { "": values.propertiesOrder } : {});
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (setDirty)
|
|
102
|
+
setDirty(dirty);
|
|
103
|
+
}, [dirty]);
|
|
104
|
+
|
|
105
|
+
const inferPropertiesFromData = doCollectionInference
|
|
106
|
+
? (): void => {
|
|
107
|
+
if (!doCollectionInference)
|
|
108
|
+
return;
|
|
109
|
+
|
|
110
|
+
setInferringProperties(true);
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
doCollectionInference(values)
|
|
113
|
+
.then((newCollection) => {
|
|
114
|
+
|
|
115
|
+
if (newCollection)
|
|
116
|
+
makePropertiesEditable(newCollection.properties as Properties);
|
|
117
|
+
|
|
118
|
+
if (!newCollection) {
|
|
119
|
+
snackbarController.open({
|
|
120
|
+
type: "error",
|
|
121
|
+
message: "Could not infer properties from data"
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// find properties in the new collection, not present in the current one
|
|
126
|
+
const newPropertyKeys = (newCollection.properties ? Object.keys(newCollection.properties) : [])
|
|
127
|
+
.filter((propertyKey) => !values.properties[propertyKey]);
|
|
128
|
+
if (newPropertyKeys.length === 0) {
|
|
129
|
+
snackbarController.open({
|
|
130
|
+
type: "info",
|
|
131
|
+
message: "No new properties found in existing data"
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// add them to the current collection
|
|
136
|
+
const updatedProperties = {
|
|
137
|
+
...newPropertyKeys.reduce((acc, propertyKey) => {
|
|
138
|
+
acc[propertyKey] = (newCollection.properties ?? {})[propertyKey];
|
|
139
|
+
return acc;
|
|
140
|
+
}, {} as { [key: string]: PropertyOrBuilder }),
|
|
141
|
+
...values.properties
|
|
142
|
+
};
|
|
143
|
+
const updatedPropertiesOrder = [
|
|
144
|
+
...newPropertyKeys,
|
|
145
|
+
...(values.propertiesOrder ?? [])
|
|
146
|
+
];
|
|
147
|
+
setFieldValue("properties", updatedProperties, false);
|
|
148
|
+
|
|
149
|
+
updatePropertiesOrder(updatedPropertiesOrder);
|
|
150
|
+
|
|
151
|
+
setInferredPropertyKeys(newPropertyKeys);
|
|
152
|
+
})
|
|
153
|
+
.finally(() => {
|
|
154
|
+
setInferringProperties(false);
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
const getCurrentPropertiesOrder = (namespace?: string) => {
|
|
160
|
+
if (!namespace) return currentPropertiesOrderRef.current[""];
|
|
161
|
+
return currentPropertiesOrderRef.current[namespace] ?? getIn(values, namespaceToPropertiesOrderPath(namespace));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const updatePropertiesOrder = (newPropertiesOrder: string[], namespace?: string) => {
|
|
165
|
+
const propertiesOrderPath = namespaceToPropertiesOrderPath(namespace);
|
|
166
|
+
|
|
167
|
+
setFieldValue(propertiesOrderPath, newPropertiesOrder, false);
|
|
168
|
+
currentPropertiesOrderRef.current[namespace ?? ""] = newPropertiesOrder;
|
|
169
|
+
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const deleteProperty = (propertyKey?: string, namespace?: string) => {
|
|
173
|
+
const fullId = propertyKey ? getFullId(propertyKey, namespace) : undefined;
|
|
174
|
+
if (!fullId)
|
|
175
|
+
throw Error("collection editor miss config");
|
|
176
|
+
|
|
177
|
+
setFieldValue(idToPropertiesPath(fullId), undefined, false);
|
|
178
|
+
|
|
179
|
+
const currentPropertiesOrder = getCurrentPropertiesOrder(namespace);
|
|
180
|
+
const newPropertiesOrder = currentPropertiesOrder.filter((p) => p !== propertyKey);
|
|
181
|
+
updatePropertiesOrder(newPropertiesOrder, namespace);
|
|
182
|
+
|
|
183
|
+
setNewPropertyDialogOpen(false);
|
|
184
|
+
|
|
185
|
+
setSelectedPropertyIndex(undefined);
|
|
186
|
+
setSelectedPropertyKey(undefined);
|
|
187
|
+
setSelectedPropertyNamespace(undefined);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const onPropertyMove = (propertiesOrder: string[], namespace?: string) => {
|
|
191
|
+
setFieldValue(namespaceToPropertiesOrderPath(namespace), propertiesOrder, false);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onPropertyCreated = ({
|
|
195
|
+
id,
|
|
196
|
+
property
|
|
197
|
+
}: {
|
|
198
|
+
id?: string,
|
|
199
|
+
property: Property
|
|
200
|
+
}) => {
|
|
201
|
+
if (!id) {
|
|
202
|
+
throw Error("Need to include an ID when creating a new property")
|
|
203
|
+
}
|
|
204
|
+
setFieldValue("properties", {
|
|
205
|
+
...(values.properties ?? {}),
|
|
206
|
+
[id]: property
|
|
207
|
+
}, false);
|
|
208
|
+
const newPropertiesOrder = [...(values.propertiesOrder ?? Object.keys(values.properties)), id];
|
|
209
|
+
|
|
210
|
+
updatePropertiesOrder(newPropertiesOrder);
|
|
211
|
+
|
|
212
|
+
setNewPropertyDialogOpen(false);
|
|
213
|
+
if (largeLayout) {
|
|
214
|
+
setSelectedPropertyIndex(newPropertiesOrder.indexOf(id));
|
|
215
|
+
setSelectedPropertyKey(id);
|
|
216
|
+
}
|
|
217
|
+
setSelectedPropertyNamespace(undefined);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const onPropertyChanged = ({
|
|
221
|
+
id,
|
|
222
|
+
property,
|
|
223
|
+
previousId,
|
|
224
|
+
namespace
|
|
225
|
+
}: OnPropertyChangedParams) => {
|
|
226
|
+
const fullId = id ? getFullId(id, namespace) : undefined;
|
|
227
|
+
const propertyPath = fullId ? idToPropertiesPath(fullId) : undefined;
|
|
228
|
+
|
|
229
|
+
// If the id has changed we need to a little cleanup
|
|
230
|
+
if (previousId && previousId !== id) {
|
|
231
|
+
const previousFullId = getFullId(previousId, namespace);
|
|
232
|
+
const previousPropertyPath = idToPropertiesPath(previousFullId);
|
|
233
|
+
|
|
234
|
+
const currentPropertiesOrder = getCurrentPropertiesOrder(namespace);
|
|
235
|
+
|
|
236
|
+
// replace previousId with id in propertiesOrder
|
|
237
|
+
const newPropertiesOrder = currentPropertiesOrder
|
|
238
|
+
.map((p) => p === previousId ? id : p)
|
|
239
|
+
.filter((p) => p !== undefined) as string[];
|
|
240
|
+
|
|
241
|
+
updatePropertiesOrder(newPropertiesOrder, namespace);
|
|
242
|
+
|
|
243
|
+
if (id) {
|
|
244
|
+
setSelectedPropertyIndex(newPropertiesOrder.indexOf(id));
|
|
245
|
+
setSelectedPropertyKey(id);
|
|
246
|
+
}
|
|
247
|
+
setFieldValue(previousPropertyPath, undefined, false);
|
|
248
|
+
setFieldTouched(previousPropertyPath, false, false);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.debug("onPropertyChanged", {
|
|
252
|
+
id,
|
|
253
|
+
property,
|
|
254
|
+
previousId,
|
|
255
|
+
namespace,
|
|
256
|
+
propertyPath
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (propertyPath) {
|
|
260
|
+
setFieldValue(propertyPath, property, false);
|
|
261
|
+
setFieldTouched(propertyPath, true, false);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const onPropertyErrorInternal = useCallback((id: string, namespace?: string, error?: FormikErrors<any>) => {
|
|
267
|
+
const propertyPath = id ? getFullId(id, namespace) : undefined;
|
|
268
|
+
console.warn("onPropertyErrorInternal", {
|
|
269
|
+
id,
|
|
270
|
+
namespace,
|
|
271
|
+
error,
|
|
272
|
+
propertyPath
|
|
273
|
+
});
|
|
274
|
+
if (propertyPath) {
|
|
275
|
+
const hasError = error && Object.keys(error).length > 0;
|
|
276
|
+
onPropertyError(id, namespace, hasError ? error : undefined);
|
|
277
|
+
setFieldError(idToPropertiesPath(propertyPath), hasError ? "Property error" : undefined);
|
|
278
|
+
}
|
|
279
|
+
}, [])
|
|
280
|
+
|
|
281
|
+
const closePropertyDialog = () => {
|
|
282
|
+
setSelectedPropertyIndex(undefined);
|
|
283
|
+
setSelectedPropertyKey(undefined);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const initialErrors = selectedPropertyKey && propertyErrorsRef?.current?.properties ? propertyErrorsRef.current.properties[selectedPropertyKey] : undefined;
|
|
287
|
+
|
|
288
|
+
const emptyCollection = values?.propertiesOrder === undefined || values.propertiesOrder.length === 0;
|
|
289
|
+
|
|
290
|
+
const usedPropertiesOrder = (values.propertiesOrder
|
|
291
|
+
? values.propertiesOrder
|
|
292
|
+
: Object.keys(values.properties)) as string[];
|
|
293
|
+
|
|
294
|
+
const owner = useMemo(() => getUser(values.ownerId), [getUser, values.ownerId]);
|
|
295
|
+
const body = (
|
|
296
|
+
<div className={"grid grid-cols-12 gap-2 h-full bg-gray-50 dark:bg-gray-900"}>
|
|
297
|
+
<div className={cn(
|
|
298
|
+
"p-4 md:p-8 pb-20 md:pb-20",
|
|
299
|
+
"col-span-12 lg:col-span-5 h-full overflow-auto",
|
|
300
|
+
!asDialog && "border-r " + defaultBorderMixin
|
|
301
|
+
)}>
|
|
302
|
+
|
|
303
|
+
<div className="flex my-2">
|
|
304
|
+
|
|
305
|
+
<div className="flex-grow mb-4">
|
|
306
|
+
|
|
307
|
+
<Field
|
|
308
|
+
name={"name"}
|
|
309
|
+
as={DebouncedTextField}
|
|
310
|
+
invisible={true}
|
|
311
|
+
className="-ml-1"
|
|
312
|
+
inputClassName="text-2xl font-headers"
|
|
313
|
+
placeholder={"Collection name"}
|
|
314
|
+
size={"small"}
|
|
315
|
+
required
|
|
316
|
+
error={Boolean(errors?.name)}/>
|
|
317
|
+
|
|
318
|
+
{owner &&
|
|
319
|
+
<Typography variant={"body2"}
|
|
320
|
+
className={"ml-2"}
|
|
321
|
+
color={"secondary"}>
|
|
322
|
+
Created by {owner.displayName}
|
|
323
|
+
</Typography>}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{extraIcon && <div className="ml-4">
|
|
327
|
+
{extraIcon}
|
|
328
|
+
</div>}
|
|
329
|
+
|
|
330
|
+
<div className="ml-1 mt-2 flex flex-row gap-2">
|
|
331
|
+
<Tooltip title={"Get the code for this collection"}>
|
|
332
|
+
<IconButton
|
|
333
|
+
variant={"filled"}
|
|
334
|
+
disabled={inferringProperties}
|
|
335
|
+
onClick={() => setCodeDialogOpen(true)}>
|
|
336
|
+
<CodeIcon/>
|
|
337
|
+
</IconButton>
|
|
338
|
+
</Tooltip>
|
|
339
|
+
{inferPropertiesFromData && <Tooltip title={"Add new properties based on data"}>
|
|
340
|
+
<IconButton
|
|
341
|
+
variant={"filled"}
|
|
342
|
+
disabled={inferringProperties}
|
|
343
|
+
onClick={inferPropertiesFromData}>
|
|
344
|
+
{inferringProperties ? <CircularProgress size={"small"}/> : <AutoAwesomeIcon/>}
|
|
345
|
+
</IconButton>
|
|
346
|
+
</Tooltip>}
|
|
347
|
+
<Tooltip title={"Add new property"}>
|
|
348
|
+
<Button
|
|
349
|
+
variant={"outlined"}
|
|
350
|
+
onClick={() => setNewPropertyDialogOpen(true)}>
|
|
351
|
+
<AddIcon/>
|
|
352
|
+
</Button>
|
|
353
|
+
</Tooltip>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<ErrorBoundary>
|
|
358
|
+
<PropertyTree
|
|
359
|
+
className={"pl-8"}
|
|
360
|
+
onPropertyClick={(propertyKey, namespace) => {
|
|
361
|
+
setSelectedPropertyIndex(usedPropertiesOrder.indexOf(propertyKey));
|
|
362
|
+
setSelectedPropertyKey(propertyKey);
|
|
363
|
+
setSelectedPropertyNamespace(namespace);
|
|
364
|
+
}}
|
|
365
|
+
inferredPropertyKeys={inferredPropertyKeys}
|
|
366
|
+
selectedPropertyKey={selectedPropertyKey ? getFullId(selectedPropertyKey, selectedPropertyNamespace) : undefined}
|
|
367
|
+
properties={values.properties}
|
|
368
|
+
additionalFields={values.additionalFields}
|
|
369
|
+
propertiesOrder={usedPropertiesOrder}
|
|
370
|
+
onPropertyMove={onPropertyMove}
|
|
371
|
+
onPropertyRemove={isNewCollection ? deleteProperty : undefined}
|
|
372
|
+
collectionEditable={collectionEditable}
|
|
373
|
+
errors={showErrors ? errors : {}}/>
|
|
374
|
+
</ErrorBoundary>
|
|
375
|
+
|
|
376
|
+
<Button className={"mt-8 w-full"}
|
|
377
|
+
color="primary"
|
|
378
|
+
variant={"outlined"}
|
|
379
|
+
size={"large"}
|
|
380
|
+
onClick={() => setNewPropertyDialogOpen(true)}
|
|
381
|
+
startIcon={<AddIcon/>}>
|
|
382
|
+
Add new property
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{!asDialog &&
|
|
387
|
+
<div className={"col-span-12 lg:col-span-7 ml-2 p-4 md:p-8 h-full overflow-auto pb-20 md:pb-20"}>
|
|
388
|
+
<Paper
|
|
389
|
+
className="sticky top-8 p-4 min-h-full border border-transparent w-full flex flex-col justify-center ">
|
|
390
|
+
|
|
391
|
+
{selectedPropertyFullId &&
|
|
392
|
+
selectedProperty &&
|
|
393
|
+
!isPropertyBuilder(selectedProperty) &&
|
|
394
|
+
<PropertyForm
|
|
395
|
+
inArray={false}
|
|
396
|
+
key={`edit_view_${selectedPropertyIndex}`}
|
|
397
|
+
existingProperty={!isNewCollection}
|
|
398
|
+
autoUpdateId={false}
|
|
399
|
+
allowDataInference={!isNewCollection}
|
|
400
|
+
autoOpenTypeSelect={false}
|
|
401
|
+
propertyKey={selectedPropertyKey}
|
|
402
|
+
propertyNamespace={selectedPropertyNamespace}
|
|
403
|
+
property={selectedProperty}
|
|
404
|
+
onPropertyChanged={onPropertyChanged}
|
|
405
|
+
onDelete={deleteProperty}
|
|
406
|
+
onError={onPropertyErrorInternal}
|
|
407
|
+
forceShowErrors={showErrors}
|
|
408
|
+
initialErrors={initialErrors}
|
|
409
|
+
getData={getData}
|
|
410
|
+
propertyConfigs={propertyConfigs}
|
|
411
|
+
collectionEditable={collectionEditable}
|
|
412
|
+
/>}
|
|
413
|
+
|
|
414
|
+
{!selectedProperty &&
|
|
415
|
+
<Typography variant={"label"} className="flex items-center justify-center h-full">
|
|
416
|
+
{emptyCollection
|
|
417
|
+
? "Now you can add your first property"
|
|
418
|
+
: "Select a property to edit it"}
|
|
419
|
+
</Typography>}
|
|
420
|
+
|
|
421
|
+
{selectedProperty && isPropertyBuilder(selectedProperty) &&
|
|
422
|
+
<Typography variant={"label"} className="flex items-center justify-center">
|
|
423
|
+
{"This property is defined as a property builder in code"}
|
|
424
|
+
</Typography>}
|
|
425
|
+
</Paper>
|
|
426
|
+
</div>}
|
|
427
|
+
|
|
428
|
+
{asDialog && <PropertyFormDialog
|
|
429
|
+
inArray={false}
|
|
430
|
+
open={selectedPropertyIndex !== undefined}
|
|
431
|
+
key={`edit_view_${selectedPropertyIndex}`}
|
|
432
|
+
autoUpdateId={!selectedProperty}
|
|
433
|
+
allowDataInference={!isNewCollection}
|
|
434
|
+
existingProperty={true}
|
|
435
|
+
autoOpenTypeSelect={false}
|
|
436
|
+
propertyKey={selectedPropertyKey}
|
|
437
|
+
propertyNamespace={selectedPropertyNamespace}
|
|
438
|
+
property={selectedProperty}
|
|
439
|
+
onPropertyChanged={onPropertyChanged}
|
|
440
|
+
onDelete={deleteProperty}
|
|
441
|
+
onError={onPropertyErrorInternal}
|
|
442
|
+
forceShowErrors={showErrors}
|
|
443
|
+
initialErrors={initialErrors}
|
|
444
|
+
getData={getData}
|
|
445
|
+
propertyConfigs={propertyConfigs}
|
|
446
|
+
collectionEditable={collectionEditable}
|
|
447
|
+
onOkClicked={asDialog
|
|
448
|
+
? closePropertyDialog
|
|
449
|
+
: undefined
|
|
450
|
+
}/>}
|
|
451
|
+
|
|
452
|
+
</div>);
|
|
453
|
+
|
|
454
|
+
return (<>
|
|
455
|
+
|
|
456
|
+
{body}
|
|
457
|
+
|
|
458
|
+
{/* This is the dialog used for new properties*/}
|
|
459
|
+
<PropertyFormDialog
|
|
460
|
+
inArray={false}
|
|
461
|
+
existingProperty={false}
|
|
462
|
+
autoOpenTypeSelect={true}
|
|
463
|
+
autoUpdateId={true}
|
|
464
|
+
forceShowErrors={showErrors}
|
|
465
|
+
open={newPropertyDialogOpen}
|
|
466
|
+
onCancel={() => setNewPropertyDialogOpen(false)}
|
|
467
|
+
onPropertyChanged={onPropertyCreated}
|
|
468
|
+
getData={getData}
|
|
469
|
+
allowDataInference={!isNewCollection}
|
|
470
|
+
propertyConfigs={propertyConfigs}
|
|
471
|
+
collectionEditable={collectionEditable}
|
|
472
|
+
existingPropertyKeys={values.propertiesOrder as string[]}/>
|
|
473
|
+
|
|
474
|
+
<GetCodeDialog
|
|
475
|
+
collection={values}
|
|
476
|
+
open={codeDialogOpen}
|
|
477
|
+
onOpenChange={setCodeDialogOpen}/>
|
|
478
|
+
|
|
479
|
+
</>
|
|
480
|
+
);
|
|
481
|
+
}
|