@firecms/collection_editor 3.0.0-3.0.0-beta.4.pre.1.0

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