@firecms/collection_editor 3.0.0-alpha.16 → 3.0.0-alpha.18

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