@firecms/collection_editor 3.0.0-alpha.9 → 3.0.0-beta.10

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