@firecms/collection_editor 3.0.0-alpha.9 → 3.0.0-beta.2-pre.1

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 (145) hide show
  1. package/dist/ConfigControllerProvider.d.ts +3 -2
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.es.js +3128 -4094
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +3 -1
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/types/collection_editor_controller.d.ts +22 -7
  8. package/dist/types/collection_inference.d.ts +1 -1
  9. package/dist/types/config_controller.d.ts +32 -5
  10. package/dist/types/persisted_collection.d.ts +3 -1
  11. package/dist/ui/CollectionViewHeaderAction.d.ts +10 -0
  12. package/dist/{components → ui}/EditorCollectionAction.d.ts +1 -1
  13. package/dist/ui/MissingReferenceWidget.d.ts +3 -0
  14. package/dist/ui/NewCollectionButton.d.ts +1 -0
  15. package/dist/ui/PropertyAddColumnComponent.d.ts +6 -0
  16. package/dist/ui/RootCollectionSuggestions.d.ts +1 -0
  17. package/dist/{components → ui}/collection_editor/CollectionDetailsForm.d.ts +3 -2
  18. package/dist/{components → ui}/collection_editor/CollectionEditorDialog.d.ts +11 -6
  19. package/dist/{components → ui}/collection_editor/CollectionPropertiesEditorForm.d.ts +6 -3
  20. package/dist/{components → ui}/collection_editor/CollectionYupValidation.d.ts +3 -0
  21. package/dist/ui/collection_editor/EntityCustomViewsSelectDialog.d.ts +4 -0
  22. package/dist/ui/collection_editor/GetCodeDialog.d.ts +5 -0
  23. package/dist/{components → ui}/collection_editor/PropertyEditView.d.ts +8 -6
  24. package/dist/{components → ui}/collection_editor/PropertyFieldPreview.d.ts +4 -3
  25. package/dist/ui/collection_editor/PropertySelectItem.d.ts +8 -0
  26. package/dist/{components → ui}/collection_editor/PropertyTree.d.ts +8 -5
  27. package/dist/{components → ui}/collection_editor/SubcollectionsEditTab.d.ts +2 -2
  28. package/dist/{components → ui}/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
  29. package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +7 -0
  30. package/dist/{components → ui}/collection_editor/import/clean_import_data.d.ts +1 -1
  31. package/dist/{components → ui}/collection_editor/properties/BlockPropertyField.d.ts +4 -1
  32. package/dist/{components → ui}/collection_editor/properties/CommonPropertyFields.d.ts +1 -0
  33. package/dist/{components → ui}/collection_editor/properties/MapPropertyField.d.ts +4 -1
  34. package/dist/{components → ui}/collection_editor/properties/RepeatPropertyField.d.ts +4 -1
  35. package/dist/{components → ui}/collection_editor/properties/StringPropertyField.d.ts +1 -1
  36. package/dist/ui/collection_editor/properties/UrlPropertyField.d.ts +4 -0
  37. package/dist/ui/collection_editor/templates/blog_template.d.ts +2 -0
  38. package/dist/ui/collection_editor/templates/pages_template.d.ts +2 -0
  39. package/dist/ui/collection_editor/templates/products_template.d.ts +2 -0
  40. package/dist/ui/collection_editor/templates/users_template.d.ts +2 -0
  41. package/dist/ui/collection_editor/utils/strings.d.ts +1 -0
  42. package/dist/ui/collection_editor/utils/supported_fields.d.ts +3 -0
  43. package/dist/ui/collection_editor/utils/update_property_for_widget.d.ts +2 -0
  44. package/dist/useCollectionEditorPlugin.d.ts +5 -3
  45. package/dist/utils/entities.d.ts +3 -4
  46. package/package.json +22 -19
  47. package/src/ConfigControllerProvider.tsx +336 -0
  48. package/src/index.ts +35 -0
  49. package/src/types/collection_editor_controller.tsx +42 -0
  50. package/src/types/collection_inference.ts +3 -0
  51. package/src/types/config_controller.tsx +60 -0
  52. package/src/types/config_permissions.ts +20 -0
  53. package/src/types/persisted_collection.ts +9 -0
  54. package/src/ui/CollectionViewHeaderAction.tsx +43 -0
  55. package/src/ui/EditorCollectionAction.tsx +109 -0
  56. package/src/ui/HomePageEditorCollectionAction.tsx +84 -0
  57. package/src/ui/MissingReferenceWidget.tsx +35 -0
  58. package/src/ui/NewCollectionButton.tsx +16 -0
  59. package/src/ui/NewCollectionCard.tsx +47 -0
  60. package/src/ui/PropertyAddColumnComponent.tsx +42 -0
  61. package/src/ui/RootCollectionSuggestions.tsx +55 -0
  62. package/src/ui/collection_editor/CollectionDetailsForm.tsx +366 -0
  63. package/src/ui/collection_editor/CollectionEditorDialog.tsx +754 -0
  64. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +206 -0
  65. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +481 -0
  66. package/src/ui/collection_editor/CollectionYupValidation.tsx +7 -0
  67. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +37 -0
  68. package/src/ui/collection_editor/EnumForm.tsx +354 -0
  69. package/src/ui/collection_editor/GetCodeDialog.tsx +110 -0
  70. package/src/ui/collection_editor/PropertyEditView.tsx +558 -0
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +203 -0
  72. package/src/ui/collection_editor/PropertySelectItem.tsx +32 -0
  73. package/src/ui/collection_editor/PropertyTree.tsx +233 -0
  74. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +253 -0
  75. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +47 -0
  76. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +37 -0
  77. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +260 -0
  78. package/src/ui/collection_editor/import/clean_import_data.ts +53 -0
  79. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +135 -0
  80. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +36 -0
  81. package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +137 -0
  82. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +87 -0
  83. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +117 -0
  84. package/src/ui/collection_editor/properties/FieldHelperView.tsx +13 -0
  85. package/src/ui/collection_editor/properties/KeyValuePropertyField.tsx +20 -0
  86. package/src/ui/collection_editor/properties/MapPropertyField.tsx +149 -0
  87. package/src/ui/collection_editor/properties/NumberPropertyField.tsx +38 -0
  88. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +165 -0
  89. package/src/ui/collection_editor/properties/RepeatPropertyField.tsx +108 -0
  90. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +194 -0
  91. package/src/ui/collection_editor/properties/StringPropertyField.tsx +79 -0
  92. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +89 -0
  93. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +36 -0
  94. package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +50 -0
  95. package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +50 -0
  96. package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +100 -0
  97. package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +132 -0
  98. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +28 -0
  99. package/src/ui/collection_editor/templates/blog_template.ts +115 -0
  100. package/src/ui/collection_editor/templates/pages_template.ts +188 -0
  101. package/src/ui/collection_editor/templates/products_template.ts +88 -0
  102. package/src/ui/collection_editor/templates/users_template.ts +42 -0
  103. package/src/ui/collection_editor/util.ts +21 -0
  104. package/src/ui/collection_editor/utils/strings.ts +8 -0
  105. package/src/ui/collection_editor/utils/supported_fields.tsx +29 -0
  106. package/src/ui/collection_editor/utils/update_property_for_widget.ts +271 -0
  107. package/src/ui/collection_editor/utils/useTraceUpdate.tsx +23 -0
  108. package/src/useCollectionEditorController.tsx +9 -0
  109. package/src/useCollectionEditorPlugin.tsx +137 -0
  110. package/src/useCollectionsConfigController.tsx +9 -0
  111. package/src/utils/arrays.ts +3 -0
  112. package/src/utils/entities.ts +38 -0
  113. package/src/vite-env.d.ts +1 -0
  114. package/dist/components/collection_editor/PropertySelectItem.d.ts +0 -8
  115. package/dist/components/collection_editor/SelectIcons.d.ts +0 -6
  116. package/dist/components/collection_editor/import/CollectionEditorImportMapping.d.ts +0 -4
  117. package/dist/components/collection_editor/templates/blog_template.d.ts +0 -10
  118. package/dist/components/collection_editor/templates/products_template.d.ts +0 -12
  119. package/dist/components/collection_editor/templates/users_template.d.ts +0 -7
  120. package/dist/components/collection_editor/utils/supported_fields.d.ts +0 -3
  121. package/dist/components/collection_editor/utils/update_property_for_widget.d.ts +0 -3
  122. package/dist/types/editable_properties.d.ts +0 -10
  123. package/dist/utils/icons.d.ts +0 -2
  124. package/dist/utils/synonyms.d.ts +0 -1951
  125. /package/dist/{components → ui}/HomePageEditorCollectionAction.d.ts +0 -0
  126. /package/dist/{components → ui}/NewCollectionCard.d.ts +0 -0
  127. /package/dist/{components → ui}/collection_editor/CollectionEditorWelcomeView.d.ts +0 -0
  128. /package/dist/{components → ui}/collection_editor/EnumForm.d.ts +0 -0
  129. /package/dist/{components → ui}/collection_editor/UnsavedChangesDialog.d.ts +0 -0
  130. /package/dist/{components → ui}/collection_editor/properties/BooleanPropertyField.d.ts +0 -0
  131. /package/dist/{components → ui}/collection_editor/properties/DateTimePropertyField.d.ts +0 -0
  132. /package/dist/{components → ui}/collection_editor/properties/EnumPropertyField.d.ts +0 -0
  133. /package/dist/{components → ui}/collection_editor/properties/FieldHelperView.d.ts +0 -0
  134. /package/dist/{components → ui}/collection_editor/properties/KeyValuePropertyField.d.ts +0 -0
  135. /package/dist/{components → ui}/collection_editor/properties/NumberPropertyField.d.ts +0 -0
  136. /package/dist/{components → ui}/collection_editor/properties/ReferencePropertyField.d.ts +0 -0
  137. /package/dist/{components → ui}/collection_editor/properties/StoragePropertyField.d.ts +0 -0
  138. /package/dist/{components → ui}/collection_editor/properties/advanced/AdvancedPropertyValidation.d.ts +0 -0
  139. /package/dist/{components → ui}/collection_editor/properties/validation/ArrayPropertyValidation.d.ts +0 -0
  140. /package/dist/{components → ui}/collection_editor/properties/validation/GeneralPropertyValidation.d.ts +0 -0
  141. /package/dist/{components → ui}/collection_editor/properties/validation/NumberPropertyValidation.d.ts +0 -0
  142. /package/dist/{components → ui}/collection_editor/properties/validation/StringPropertyValidation.d.ts +0 -0
  143. /package/dist/{components → ui}/collection_editor/properties/validation/ValidationPanel.d.ts +0 -0
  144. /package/dist/{components → ui}/collection_editor/util.d.ts +0 -0
  145. /package/dist/{components → ui}/collection_editor/utils/useTraceUpdate.d.ts +0 -0
@@ -0,0 +1,754 @@
1
+ import * as React from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import {
4
+ CircularProgressCenter,
5
+ CMSType,
6
+ EntityCollection,
7
+ ErrorView,
8
+ isPropertyBuilder,
9
+ MapProperty,
10
+ mergeDeep,
11
+ Properties,
12
+ PropertiesOrBuilders,
13
+ Property,
14
+ PropertyConfig,
15
+ PropertyOrBuilder,
16
+ randomString,
17
+ removeInitialAndTrailingSlashes,
18
+ removeUndefined,
19
+ TopNavigationResult,
20
+ useAuthController,
21
+ useCustomizationController,
22
+ useNavigationController,
23
+ User,
24
+ useSnackbarController
25
+ } from "@firecms/core";
26
+ import {
27
+ ArrowBackIcon,
28
+ Button,
29
+ cn,
30
+ coolIconKeys,
31
+ defaultBorderMixin,
32
+ Dialog,
33
+ DialogActions,
34
+ DialogContent,
35
+ DoneIcon,
36
+ IconButton,
37
+ LoadingButton,
38
+ Tab,
39
+ Tabs,
40
+ } from "@firecms/ui";
41
+ import { Form, Formik, FormikHelpers } from "formik";
42
+ import { YupSchema } from "./CollectionYupValidation";
43
+ import { CollectionDetailsForm } from "./CollectionDetailsForm";
44
+ import { CollectionPropertiesEditorForm } from "./CollectionPropertiesEditorForm";
45
+ import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
46
+ import { SubcollectionsEditTab } from "./SubcollectionsEditTab";
47
+ import { CollectionsConfigController } from "../../types/config_controller";
48
+ import { CollectionEditorWelcomeView } from "./CollectionEditorWelcomeView";
49
+ import { CollectionInference } from "../../types/collection_inference";
50
+ import { getInferenceType, ImportSaveInProgress, useImportConfig } from "@firecms/data_import_export";
51
+ import { buildEntityPropertiesFromData } from "@firecms/schema_inference";
52
+ import { CollectionEditorImportMapping } from "./import/CollectionEditorImportMapping";
53
+ import { CollectionEditorImportDataPreview } from "./import/CollectionEditorImportDataPreview";
54
+ import { cleanPropertiesFromImport } from "./import/clean_import_data";
55
+ import { PersistedCollection } from "../../types/persisted_collection";
56
+
57
+ export interface CollectionEditorDialogProps {
58
+ open: boolean;
59
+ isNewCollection: boolean;
60
+ initialValues?: {
61
+ group?: string,
62
+ path?: string,
63
+ name?: string,
64
+ }
65
+ editedCollectionPath?: string; // last segment of the path, like `locales`
66
+ fullPath?: string; // full path of this particular collection, like `products/123/locales`
67
+ parentCollectionIds?: string[]; // path ids of the parent collection, like [`products`]
68
+ handleClose: (collection?: EntityCollection) => void;
69
+ configController: CollectionsConfigController;
70
+ reservedGroups?: string[];
71
+ collectionInference?: CollectionInference;
72
+ extraView?: {
73
+ View: React.ComponentType<{
74
+ path: string
75
+ }>,
76
+ icon: React.ReactNode
77
+ };
78
+ pathSuggestions?: (path?: string) => Promise<string[]>;
79
+ getUser: (uid: string) => User | null;
80
+ getData?: (path: string, parentPaths: string[]) => Promise<object[]>;
81
+ parentCollection?: PersistedCollection;
82
+ }
83
+
84
+ export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
85
+
86
+ const open = props.open;
87
+
88
+ const [formDirty, setFormDirty] = React.useState<boolean>(false);
89
+ const [unsavedChangesDialogOpen, setUnsavedChangesDialogOpen] = React.useState<boolean>(false);
90
+
91
+ const handleCancel = useCallback(() => {
92
+ if (!formDirty) {
93
+ props.handleClose(undefined);
94
+ } else {
95
+ setUnsavedChangesDialogOpen(true);
96
+ }
97
+ }, [formDirty, props.handleClose]);
98
+
99
+ useEffect(() => {
100
+ if (!open) {
101
+ setFormDirty(false);
102
+ setUnsavedChangesDialogOpen(false);
103
+ }
104
+ }, [open]);
105
+
106
+ return (
107
+ <Dialog
108
+ open={open}
109
+ fullWidth={true}
110
+ fullHeight={true}
111
+ scrollable={false}
112
+ maxWidth={"7xl"}
113
+ onOpenChange={(open) => !open ? handleCancel() : undefined}
114
+ >
115
+ {open && <CollectionEditorDialogInternal {...props}
116
+ handleCancel={handleCancel}
117
+ setFormDirty={setFormDirty}/>}
118
+
119
+ <UnsavedChangesDialog
120
+ open={unsavedChangesDialogOpen}
121
+ handleOk={() => props.handleClose(undefined)}
122
+ handleCancel={() => setUnsavedChangesDialogOpen(false)}
123
+ body={"There are unsaved changes in this collection"}/>
124
+
125
+ </Dialog>
126
+ );
127
+ }
128
+
129
+ type EditorView = "welcome"
130
+ | "details"
131
+ | "import_data_mapping"
132
+ | "import_data_preview"
133
+ | "import_data_saving"
134
+ | "properties"
135
+ | "loading"
136
+ | "extra_view"
137
+ | "subcollections";
138
+
139
+ export function CollectionEditorDialogInternal<M extends {
140
+ [Key: string]: CMSType
141
+ }>({
142
+ isNewCollection,
143
+ initialValues: initialValuesProp,
144
+ configController,
145
+ editedCollectionPath,
146
+ parentCollectionIds,
147
+ fullPath,
148
+ collectionInference,
149
+ handleClose,
150
+ reservedGroups,
151
+ extraView,
152
+ handleCancel,
153
+ setFormDirty,
154
+ pathSuggestions,
155
+ getUser,
156
+ parentCollection,
157
+ getData
158
+ }: CollectionEditorDialogProps & {
159
+ handleCancel: () => void,
160
+ setFormDirty: (dirty: boolean) => void
161
+ }
162
+ ) {
163
+
164
+ const { propertyConfigs } = useCustomizationController();
165
+ const navigation = useNavigationController();
166
+ const {
167
+ topLevelNavigation,
168
+ collections
169
+ } = navigation;
170
+
171
+ const includeTemplates = !initialValuesProp?.path && (parentCollectionIds ?? []).length === 0;
172
+ const collectionsInThisLevel = (parentCollection ? parentCollection.subcollections : collections) ?? [];
173
+ const existingPaths = collectionsInThisLevel.map(col => col.path.trim().toLowerCase());
174
+ const existingIds = collectionsInThisLevel.map(col => col.id?.trim().toLowerCase()).filter(Boolean) as string[];
175
+
176
+ const importConfig = useImportConfig();
177
+
178
+ if (!topLevelNavigation) {
179
+ throw Error("Internal: Navigation not ready in collection editor");
180
+ }
181
+
182
+ const {
183
+ groups
184
+ }: TopNavigationResult = topLevelNavigation;
185
+
186
+ const snackbarController = useSnackbarController();
187
+ const authController = useAuthController();
188
+
189
+ // Use this ref to store which properties have errors
190
+ const propertyErrorsRef = useRef({});
191
+
192
+ const initialView = isNewCollection ? (includeTemplates ? "welcome" : "details") : "properties";
193
+ const [currentView, setCurrentView] = useState<EditorView>(initialView); // this view can edit either the details view or the properties one
194
+
195
+ const [error, setError] = React.useState<Error | undefined>();
196
+
197
+ const [collection, setCollection] = React.useState<PersistedCollection<M> | undefined>();
198
+ const [initialLoadingCompleted, setInitialLoadingCompleted] = React.useState(false);
199
+ const [initialError, setInitialError] = React.useState<Error | undefined>();
200
+
201
+ useEffect(() => {
202
+ try {
203
+ if (navigation.initialised) {
204
+ if (editedCollectionPath) {
205
+ setCollection(navigation.getCollectionFromPaths<PersistedCollection<M>>([...(parentCollectionIds ?? []), editedCollectionPath]));
206
+ } else {
207
+ setCollection(undefined);
208
+ }
209
+ setInitialLoadingCompleted(true);
210
+ }
211
+ } catch (e) {
212
+ console.error(e);
213
+ setInitialError(initialError);
214
+ }
215
+ }, [navigation.getCollectionFromPaths, editedCollectionPath, initialError, navigation.initialised]);
216
+
217
+ const saveCollection = (updatedCollection: PersistedCollection<M>): Promise<boolean> => {
218
+ const fullPath = updatedCollection.id || updatedCollection.path;
219
+ return configController.saveCollection({
220
+ id: fullPath,
221
+ collectionData: updatedCollection,
222
+ previousPath: editedCollectionPath,
223
+ parentCollectionIds
224
+ })
225
+ .then(() => {
226
+ setError(undefined);
227
+ return true;
228
+ })
229
+ .catch((e) => {
230
+ setError(e);
231
+ console.error(e);
232
+ snackbarController.open({
233
+ type: "error",
234
+ message: "Error persisting collection: " + (e.message ?? "Details in the console")
235
+ });
236
+ return false;
237
+ });
238
+ };
239
+
240
+ const initialCollection = collection
241
+ ? {
242
+ ...collection,
243
+ id: collection.id ?? collection.path ?? randomString(16)
244
+ }
245
+ : undefined;
246
+
247
+ const initialValues: PersistedCollection<M> = initialCollection
248
+ ? applyPropertyConfigs(initialCollection, propertyConfigs)
249
+ : {
250
+ id: initialValuesProp?.path ?? randomString(16),
251
+ path: initialValuesProp?.path ?? "",
252
+ name: initialValuesProp?.name ?? "",
253
+ group: initialValuesProp?.group ?? "",
254
+ properties: {} as PropertiesOrBuilders<M>,
255
+ propertiesOrder: [],
256
+ icon: coolIconKeys[Math.floor(Math.random() * coolIconKeys.length)],
257
+ ownerId: authController.user?.uid ?? ""
258
+ };
259
+
260
+ const setNextMode = useCallback(() => {
261
+ if (currentView === "details") {
262
+ if (importConfig.inUse) {
263
+ setCurrentView("import_data_saving");
264
+ } else if (extraView) {
265
+ setCurrentView("extra_view");
266
+ } else {
267
+ setCurrentView("properties");
268
+ }
269
+ } else if (currentView === "welcome") {
270
+ setCurrentView("details");
271
+ } else if (currentView === "import_data_mapping") {
272
+ setCurrentView("import_data_preview");
273
+ } else if (currentView === "import_data_preview") {
274
+ setCurrentView("details");
275
+ } else if (currentView === "extra_view") {
276
+ setCurrentView("properties");
277
+ } else {
278
+ setCurrentView("details");
279
+ }
280
+
281
+ }, [currentView, importConfig.inUse, extraView]);
282
+
283
+ const doCollectionInference = useCallback((collection: PersistedCollection<any>) => {
284
+ if (!collectionInference) return undefined;
285
+ return collectionInference?.(collection.path, collection.collectionGroup ?? false, parentCollectionIds ?? []);
286
+ }, [collectionInference, parentCollectionIds]);
287
+
288
+ const inferCollectionFromData = useCallback(async (newCollection: PersistedCollection<M>) => {
289
+
290
+ try {
291
+ if (!doCollectionInference) {
292
+ setCollection(newCollection);
293
+ return Promise.resolve(newCollection);
294
+ }
295
+
296
+ setCurrentView("loading");
297
+
298
+ const inferredCollection = await doCollectionInference?.(newCollection);
299
+
300
+ if (!inferredCollection) {
301
+ setCollection(newCollection);
302
+ return Promise.resolve(newCollection);
303
+ }
304
+ const values = {
305
+ ...(newCollection ?? {})
306
+ };
307
+
308
+ if (Object.keys(inferredCollection.properties ?? {}).length > 0) {
309
+ values.properties = inferredCollection.properties as PropertiesOrBuilders<M>;
310
+ values.propertiesOrder = inferredCollection.propertiesOrder as Extract<keyof M, string>[];
311
+ }
312
+
313
+ if (!values.propertiesOrder) {
314
+ values.propertiesOrder = Object.keys(values.properties) as Extract<keyof M, string>[];
315
+ return values;
316
+ }
317
+
318
+ setCollection(values);
319
+ console.log("Inferred collection", {
320
+ newCollection: newCollection ?? {},
321
+ values
322
+ });
323
+ return values;
324
+ } catch (e: any) {
325
+ console.error(e);
326
+ snackbarController.open({
327
+ type: "error",
328
+ message: "Error inferring collection: " + (e.message ?? "Details in the console")
329
+ });
330
+ return newCollection;
331
+ }
332
+ }, [parentCollectionIds, doCollectionInference]);
333
+
334
+ const onSubmit = (newCollectionState: PersistedCollection<M>, formikHelpers: FormikHelpers<PersistedCollection<M>>) => {
335
+ try {
336
+
337
+ console.log("Submitting collection", newCollectionState)
338
+ if (!isNewCollection) {
339
+ saveCollection(newCollectionState).then(() => {
340
+ formikHelpers.resetForm({ values: initialValues });
341
+ // setNextMode();
342
+ handleClose(newCollectionState);
343
+ });
344
+ return;
345
+ }
346
+
347
+ if (currentView === "welcome") {
348
+ setNextMode();
349
+ formikHelpers.resetForm({ values: newCollectionState });
350
+ } else if (currentView === "details") {
351
+ if (extraView || importConfig.inUse) {
352
+ formikHelpers.resetForm({ values: newCollectionState });
353
+ setNextMode();
354
+ } else if (isNewCollection) {
355
+ inferCollectionFromData(newCollectionState)
356
+ .then((values) => {
357
+ formikHelpers.resetForm({
358
+ values: values ?? newCollectionState,
359
+ touched: {
360
+ path: true,
361
+ name: true
362
+ }
363
+ });
364
+ }).finally(() => {
365
+ setNextMode();
366
+ });
367
+ } else {
368
+ formikHelpers.resetForm({ values: newCollectionState });
369
+ setNextMode();
370
+ }
371
+ } else if (currentView === "extra_view") {
372
+ setNextMode();
373
+ formikHelpers.resetForm({ values: newCollectionState });
374
+ } else if (currentView === "import_data_mapping") {
375
+ setNextMode();
376
+ } else if (currentView === "import_data_preview") {
377
+ setNextMode();
378
+ } else if (currentView === "properties") {
379
+ saveCollection(newCollectionState).then(() => {
380
+ formikHelpers.resetForm({ values: initialValues });
381
+ setNextMode();
382
+ handleClose(newCollectionState);
383
+ });
384
+ } else {
385
+ setNextMode();
386
+ formikHelpers.resetForm({ values: newCollectionState });
387
+ }
388
+ } catch (e: any) {
389
+ snackbarController.open({
390
+ type: "error",
391
+ message: "Error persisting collection: " + (e.message ?? "Details in the console")
392
+ });
393
+ console.error(e);
394
+ formikHelpers.resetForm({ values: newCollectionState });
395
+ }
396
+ };
397
+
398
+ if (!isNewCollection && (!navigation.initialised || !initialLoadingCompleted)) {
399
+ return <CircularProgressCenter/>;
400
+ }
401
+
402
+ return <DialogContent fullHeight={true}>
403
+ <Formik
404
+ initialValues={initialValues}
405
+ validationSchema={(currentView === "properties" || currentView === "subcollections" || currentView === "details") && YupSchema}
406
+ validate={(v) => {
407
+ if (currentView === "properties") {
408
+ // return the errors for the properties form
409
+ return propertyErrorsRef.current;
410
+ }
411
+ const errors: Record<string, any> = {};
412
+ if (currentView === "details") {
413
+ const pathError = validatePath(v.path, isNewCollection, existingPaths, v.id);
414
+ if (pathError) {
415
+ errors.path = pathError;
416
+ }
417
+ const idError = validateId(v.id, isNewCollection, existingPaths, existingIds);
418
+ if (idError) {
419
+ errors.id = idError;
420
+ }
421
+ }
422
+ return errors;
423
+ }}
424
+ onSubmit={onSubmit}
425
+ >
426
+ {(formikHelpers) => {
427
+ const {
428
+ values,
429
+ errors,
430
+ setFieldValue,
431
+ isSubmitting,
432
+ dirty,
433
+ submitCount
434
+ } = formikHelpers;
435
+
436
+ const path = values.path ?? editedCollectionPath;
437
+ const updatedFullPath = fullPath?.includes("/") ? fullPath?.split("/").slice(0, -1).join("/") + "/" + path : path; // TODO: this path is wrong
438
+ const pathError = validatePath(path, isNewCollection, existingPaths, values.id);
439
+
440
+ const parentPaths = !pathError && parentCollectionIds ? navigation.convertIdsToPaths(parentCollectionIds) : undefined;
441
+ const resolvedPath = !pathError ? navigation.resolveAliasesFrom(updatedFullPath) : undefined;
442
+ const getDataWithPath = resolvedPath && getData ? () => getData(resolvedPath, parentPaths ?? []) : undefined;
443
+
444
+ // eslint-disable-next-line react-hooks/rules-of-hooks
445
+ useEffect(() => {
446
+ setFormDirty(dirty);
447
+ }, [dirty]);
448
+
449
+ function onImportDataSet(data: object[]) {
450
+ importConfig.setInUse(true);
451
+ buildEntityPropertiesFromData(data, getInferenceType)
452
+ .then((properties) => {
453
+ const res = cleanPropertiesFromImport(properties);
454
+
455
+ setFieldValue("properties", res.properties);
456
+ setFieldValue("propertiesOrder", Object.keys(res.properties));
457
+
458
+ importConfig.setIdColumn(res.idColumn);
459
+ importConfig.setImportData(data);
460
+ importConfig.setHeadersMapping(res.headersMapping);
461
+ importConfig.setOriginProperties(res.properties);
462
+ });
463
+ }
464
+
465
+ const validValues = Boolean(values.name) && Boolean(values.id);
466
+
467
+ const onImportMappingComplete = () => {
468
+ const updatedProperties = { ...values.properties };
469
+ if (importConfig.idColumn)
470
+ delete updatedProperties[importConfig.idColumn];
471
+ setFieldValue("properties", updatedProperties);
472
+ // setFieldValue("propertiesOrder", Object.values(importConfig.headersMapping));
473
+ setNextMode();
474
+ };
475
+
476
+ const editable = collection?.editable === undefined || collection?.editable === true;
477
+ const collectionEditable = editable || isNewCollection;
478
+ return (
479
+ <>
480
+ {!isNewCollection && <Tabs value={currentView}
481
+ className={cn(defaultBorderMixin, "justify-end bg-gray-50 dark:bg-gray-950 border-b")}
482
+ onValueChange={(v) => setCurrentView(v as EditorView)}>
483
+ <Tab value={"details"}>
484
+ Details
485
+ </Tab>
486
+ <Tab value={"properties"}>
487
+ Properties
488
+ </Tab>
489
+ <Tab value={"subcollections"}>
490
+ Additional views
491
+ </Tab>
492
+ </Tabs>}
493
+
494
+ <Form noValidate
495
+ className={cn(
496
+ isNewCollection ? "h-full" : "h-[calc(100%-48px)]",
497
+ "flex-grow flex flex-col relative")}>
498
+
499
+ {currentView === "loading" &&
500
+ <CircularProgressCenter/>}
501
+
502
+ {currentView === "extra_view" &&
503
+ path &&
504
+ extraView?.View &&
505
+ <extraView.View path={path}/>}
506
+
507
+ {currentView === "welcome" &&
508
+ <CollectionEditorWelcomeView
509
+ path={path}
510
+ onContinue={(data) => {
511
+ if (data) {
512
+ onImportDataSet(data);
513
+ setCurrentView("import_data_mapping");
514
+ } else {
515
+ setCurrentView("details");
516
+ }
517
+ }}
518
+ collections={collections}
519
+ parentCollection={parentCollection}
520
+ pathSuggestions={pathSuggestions}/>}
521
+
522
+ {currentView === "import_data_mapping" && importConfig &&
523
+ <CollectionEditorImportMapping importConfig={importConfig}
524
+ collectionEditable={collectionEditable}
525
+ propertyConfigs={propertyConfigs}/>}
526
+
527
+ {currentView === "import_data_preview" && importConfig &&
528
+ <CollectionEditorImportDataPreview importConfig={importConfig}
529
+ properties={values.properties as Properties}
530
+ propertiesOrder={values.propertiesOrder as string[]}/>}
531
+
532
+ {currentView === "import_data_saving" && importConfig &&
533
+ <ImportSaveInProgress importConfig={importConfig}
534
+ collection={values}
535
+ onImportSuccess={(importedCollection) => {
536
+ handleClose(importedCollection);
537
+ snackbarController.open({
538
+ type: "info",
539
+ message: "Data imported successfully"
540
+ });
541
+ }}
542
+ />}
543
+
544
+ {currentView === "details" &&
545
+ <CollectionDetailsForm
546
+ existingPaths={existingPaths}
547
+ existingIds={existingIds}
548
+ groups={groups}
549
+ parentCollectionIds={parentCollectionIds}
550
+ parentCollection={parentCollection}
551
+ isNewCollection={isNewCollection}/>}
552
+
553
+ {currentView === "subcollections" && collection &&
554
+ <SubcollectionsEditTab
555
+ parentCollection={parentCollection}
556
+ configController={configController}
557
+ getUser={getUser}
558
+ collectionInference={collectionInference}
559
+ parentCollectionIds={parentCollectionIds}
560
+ collection={collection}/>}
561
+
562
+ {currentView === "properties" &&
563
+ <CollectionPropertiesEditorForm
564
+ showErrors={submitCount > 0}
565
+ isNewCollection={isNewCollection}
566
+ reservedGroups={reservedGroups}
567
+ onPropertyError={(propertyKey, namespace, error) => {
568
+ propertyErrorsRef.current = removeUndefined({
569
+ ...propertyErrorsRef.current,
570
+ [propertyKey]: error
571
+ }, true);
572
+ }}
573
+ getUser={getUser}
574
+ getData={getDataWithPath}
575
+ doCollectionInference={doCollectionInference}
576
+ propertyConfigs={propertyConfigs}
577
+ collectionEditable={collectionEditable}
578
+ extraIcon={extraView?.icon &&
579
+ <IconButton
580
+ color={"primary"}
581
+ onClick={() => setCurrentView("extra_view")}>
582
+ {extraView.icon}
583
+ </IconButton>}/>
584
+ }
585
+
586
+ {currentView !== "welcome" && <DialogActions
587
+ position={"absolute"}>
588
+ {error && <ErrorView error={error}/>}
589
+
590
+ {isNewCollection && includeTemplates && currentView === "import_data_mapping" &&
591
+ <Button variant={"text"}
592
+ type="button"
593
+ onClick={() => {
594
+ importConfig.setInUse(false);
595
+ return setCurrentView("welcome");
596
+ }}>
597
+ <ArrowBackIcon/>
598
+ Back
599
+ </Button>}
600
+
601
+ {isNewCollection && includeTemplates && currentView === "import_data_preview" &&
602
+ <Button variant={"text"}
603
+ type="button"
604
+ onClick={() => {
605
+ saveCollection(values);
606
+ setCurrentView("import_data_mapping");
607
+ }}>
608
+ <ArrowBackIcon/>
609
+ Back
610
+ </Button>}
611
+
612
+ {isNewCollection && includeTemplates && currentView === "details" &&
613
+ <Button variant={"text"}
614
+ type="button"
615
+ onClick={() => setCurrentView("welcome")}>
616
+ <ArrowBackIcon/>
617
+ Back
618
+ </Button>}
619
+
620
+ {isNewCollection && currentView === "properties" && <Button variant={"text"}
621
+ type="button"
622
+ onClick={() => setCurrentView("details")}>
623
+ <ArrowBackIcon/>
624
+ Back
625
+ </Button>}
626
+
627
+ <Button variant={"text"}
628
+ onClick={() => {
629
+ handleCancel();
630
+ }}>
631
+ Cancel
632
+ </Button>
633
+
634
+ {isNewCollection && currentView === "import_data_mapping" &&
635
+ <Button
636
+ variant={"filled"}
637
+ color="primary"
638
+ onClick={onImportMappingComplete}
639
+ >
640
+ Next
641
+ </Button>}
642
+
643
+ {isNewCollection && currentView === "import_data_preview" &&
644
+ <Button
645
+ variant={"filled"}
646
+ color="primary"
647
+ onClick={() => {
648
+ setNextMode();
649
+ }}
650
+ >
651
+ Next
652
+ </Button>}
653
+
654
+ {isNewCollection && (currentView === "details" || currentView === "properties") &&
655
+ <LoadingButton
656
+ variant={"filled"}
657
+ color="primary"
658
+ type="submit"
659
+ loading={isSubmitting}
660
+ disabled={isSubmitting || (currentView === "details" && !validValues)}
661
+ startIcon={currentView === "properties"
662
+ ? <DoneIcon/>
663
+ : undefined}
664
+ >
665
+ {currentView === "details" && "Next"}
666
+ {currentView === "properties" && "Create collection"}
667
+ </LoadingButton>}
668
+
669
+ {!isNewCollection && <LoadingButton
670
+ variant="filled"
671
+ color="primary"
672
+ type="submit"
673
+ loading={isSubmitting}
674
+ // disabled={isSubmitting || !dirty}
675
+ >
676
+ Update collection
677
+ </LoadingButton>}
678
+
679
+ </DialogActions>}
680
+ </Form>
681
+ </>
682
+ );
683
+ }}
684
+
685
+ </Formik>
686
+ </DialogContent>
687
+
688
+ }
689
+
690
+ function applyPropertyConfigs<M extends Record<string, any> = any>(collection: PersistedCollection<M>, propertyConfigs: Record<string, PropertyConfig<any>>): PersistedCollection<M> {
691
+ const { properties, ...rest } = collection;
692
+ const propertiesResult: PropertiesOrBuilders<any> = {};
693
+ Object.keys(properties).forEach((key) => {
694
+ propertiesResult[key] = applyPropertiesConfig(properties[key] as PropertyOrBuilder, propertyConfigs);
695
+ });
696
+
697
+ return { ...rest, properties: propertiesResult };
698
+ }
699
+
700
+ function applyPropertiesConfig(property: PropertyOrBuilder, propertyConfigs: Record<string, PropertyConfig<any>>) {
701
+ let internalProperty = property;
702
+ if (propertyConfigs && typeof internalProperty === "object" && internalProperty.propertyConfig) {
703
+ const propertyConfig = propertyConfigs[internalProperty.propertyConfig];
704
+ if (propertyConfig && isPropertyBuilder(propertyConfig.property)) {
705
+ internalProperty = propertyConfig.property;
706
+ } else {
707
+
708
+ if (propertyConfig) {
709
+ internalProperty = mergeDeep(propertyConfig.property, internalProperty);
710
+ }
711
+
712
+ if (!isPropertyBuilder(internalProperty) && internalProperty.dataType === "map" && internalProperty.properties) {
713
+ const properties: Record<string, PropertyOrBuilder> = {};
714
+ Object.keys(internalProperty.properties).forEach((key) => {
715
+ properties[key] = applyPropertiesConfig(((internalProperty as MapProperty).properties as Properties)[key] as Property, propertyConfigs);
716
+ });
717
+ internalProperty = { ...internalProperty, properties };
718
+ }
719
+
720
+ }
721
+ }
722
+ return internalProperty;
723
+
724
+ }
725
+
726
+ const validatePath = (value: string, isNewCollection: boolean, existingPaths: string[], idValue?: string) => {
727
+ let error;
728
+ if (!value) {
729
+ error = "You must specify a path in the database for this collection";
730
+ }
731
+ // if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
732
+ // error = "There is already a collection which uses this path as an id";
733
+ if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()) && !idValue)
734
+ error = "There is already a collection with the specified path. If you want to have multiple collections referring to the same database path, make sure the have different ids";
735
+
736
+ const subpaths = removeInitialAndTrailingSlashes(value).split("/");
737
+ if (subpaths.length % 2 === 0) {
738
+ error = `Collection paths must have an odd number of segments: ${value}`;
739
+ }
740
+ return error;
741
+ };
742
+
743
+ const validateId = (value: string, isNewCollection: boolean, existingPaths: string[], existingIds: string[]) => {
744
+ if (!value) return undefined;
745
+ let error;
746
+ if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()))
747
+ error = "There is already a collection that uses this value as a path";
748
+ if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
749
+ error = "There is already a collection which uses this id";
750
+ // if (error) {
751
+ // setAdvancedPanelExpanded(true);
752
+ // }
753
+ return error;
754
+ };