@firecms/collection_editor 3.0.0-alpha.5 → 3.0.0-alpha.50

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