@firecms/core 3.0.0-beta.11 → 3.0.0-beta.13

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 (201) hide show
  1. package/dist/app/Scaffold.d.ts +4 -0
  2. package/dist/components/ArrayContainer.d.ts +14 -3
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
  6. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityJsonPreview.d.ts +3 -0
  9. package/dist/components/PropertyIdCopyTooltip.d.ts +1 -1
  10. package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
  11. package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +1 -1
  12. package/dist/components/UnsavedChangesDialog.d.ts +8 -0
  13. package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
  14. package/dist/components/common/default_entity_actions.d.ts +0 -2
  15. package/dist/components/common/index.d.ts +1 -1
  16. package/dist/components/common/useColumnsIds.d.ts +1 -0
  17. package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
  18. package/dist/components/common/useDebouncedCallback.d.ts +1 -0
  19. package/dist/components/common/useScrollRestoration.d.ts +14 -0
  20. package/dist/contexts/BreacrumbsContext.d.ts +8 -0
  21. package/dist/core/DefaultAppBar.d.ts +8 -2
  22. package/dist/core/EntityEditView.d.ts +36 -21
  23. package/dist/core/EntityEditViewFormActions.d.ts +2 -0
  24. package/dist/core/FireCMS.d.ts +2 -2
  25. package/dist/core/FireCMSRouter.d.ts +4 -0
  26. package/dist/core/NavigationRoutes.d.ts +0 -1
  27. package/dist/core/SideDialogs.d.ts +4 -2
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/form/EntityForm.d.ts +49 -0
  30. package/dist/form/EntityFormActions.d.ts +17 -0
  31. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  32. package/dist/form/components/FormEntry.d.ts +6 -0
  33. package/dist/form/components/FormLayout.d.ts +5 -0
  34. package/dist/form/components/index.d.ts +2 -0
  35. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  36. package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
  37. package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
  38. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  39. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  40. package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
  41. package/dist/form/index.d.ts +16 -16
  42. package/dist/hooks/data/save.d.ts +1 -1
  43. package/dist/hooks/data/useEntityFetch.d.ts +2 -1
  44. package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
  45. package/dist/hooks/useBuildNavigationController.d.ts +4 -1
  46. package/dist/hooks/useModeController.d.ts +1 -2
  47. package/dist/index.es.js +15842 -14396
  48. package/dist/index.es.js.map +1 -1
  49. package/dist/index.umd.js +15833 -14389
  50. package/dist/index.umd.js.map +1 -1
  51. package/dist/internal/useBuildDataSource.d.ts +3 -2
  52. package/dist/internal/useBuildSideEntityController.d.ts +3 -3
  53. package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
  54. package/dist/routes/CustomCMSRoute.d.ts +4 -0
  55. package/dist/routes/FireCMSRoute.d.ts +1 -0
  56. package/dist/routes/HomePageRoute.d.ts +3 -0
  57. package/dist/types/collections.d.ts +36 -10
  58. package/dist/types/datasource.d.ts +2 -2
  59. package/dist/types/dialogs_controller.d.ts +7 -3
  60. package/dist/types/entities.d.ts +1 -1
  61. package/dist/types/entity_actions.d.ts +10 -0
  62. package/dist/types/fields.d.ts +13 -3
  63. package/dist/types/firecms.d.ts +1 -1
  64. package/dist/types/navigation.d.ts +33 -12
  65. package/dist/types/plugins.d.ts +2 -0
  66. package/dist/types/properties.d.ts +40 -4
  67. package/dist/types/side_entity_controller.d.ts +6 -1
  68. package/dist/util/builders.d.ts +1 -1
  69. package/dist/util/createFormexStub.d.ts +2 -0
  70. package/dist/util/entity_actions.d.ts +2 -0
  71. package/dist/util/entity_cache.d.ts +23 -0
  72. package/dist/util/index.d.ts +1 -0
  73. package/dist/util/navigation_from_path.d.ts +6 -1
  74. package/dist/util/navigation_utils.d.ts +13 -1
  75. package/dist/util/objects.d.ts +1 -1
  76. package/dist/util/property_utils.d.ts +2 -2
  77. package/dist/util/references.d.ts +2 -2
  78. package/dist/util/resolutions.d.ts +12 -2
  79. package/package.json +20 -21
  80. package/src/app/Scaffold.tsx +13 -3
  81. package/src/components/ArrayContainer.tsx +60 -24
  82. package/src/components/CircularProgressCenter.tsx +1 -1
  83. package/src/components/DeleteEntityDialog.tsx +5 -3
  84. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +46 -23
  85. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
  86. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
  87. package/src/components/EntityCollectionTable/index.tsx +1 -1
  88. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  89. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +10 -13
  90. package/src/components/EntityCollectionView/EntityCollectionView.tsx +105 -47
  91. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -2
  92. package/src/components/EntityJsonPreview.tsx +66 -0
  93. package/src/components/EntityPreview.tsx +10 -3
  94. package/src/components/EntityView.tsx +4 -1
  95. package/src/components/NotFoundPage.tsx +2 -2
  96. package/src/components/PropertyIdCopyTooltip.tsx +2 -3
  97. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +14 -7
  98. package/src/components/SelectableTable/SelectableTable.tsx +18 -3
  99. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +7 -7
  100. package/src/components/UnsavedChangesDialog.tsx +42 -0
  101. package/src/components/VirtualTable/VirtualTable.tsx +48 -3
  102. package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
  103. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
  104. package/src/components/common/default_entity_actions.tsx +37 -37
  105. package/src/components/common/index.ts +1 -1
  106. package/src/components/common/useColumnsIds.tsx +1 -1
  107. package/src/components/common/useDataSourceTableController.tsx +420 -0
  108. package/src/components/common/useDebouncedCallback.tsx +20 -0
  109. package/src/components/common/useScrollRestoration.tsx +68 -0
  110. package/src/contexts/BreacrumbsContext.tsx +38 -0
  111. package/src/contexts/DialogsProvider.tsx +3 -2
  112. package/src/contexts/ModeController.tsx +1 -3
  113. package/src/core/DefaultAppBar.tsx +59 -17
  114. package/src/core/DefaultDrawer.tsx +1 -1
  115. package/src/core/EntityEditView.tsx +354 -1000
  116. package/src/core/EntityEditViewFormActions.tsx +199 -0
  117. package/src/core/EntitySidePanel.tsx +85 -15
  118. package/src/core/FireCMS.tsx +20 -16
  119. package/src/core/FireCMSRouter.tsx +17 -0
  120. package/src/core/NavigationRoutes.tsx +23 -32
  121. package/src/core/SideDialogs.tsx +20 -11
  122. package/src/core/index.tsx +4 -2
  123. package/src/form/EntityForm.tsx +782 -0
  124. package/src/form/EntityFormActions.tsx +169 -0
  125. package/src/form/PropertyFieldBinding.tsx +21 -16
  126. package/src/form/components/FormEntry.tsx +22 -0
  127. package/src/form/components/FormLayout.tsx +16 -0
  128. package/src/form/components/LabelWithIcon.tsx +6 -3
  129. package/src/form/components/StorageUploadProgress.tsx +1 -1
  130. package/src/form/components/index.tsx +2 -0
  131. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +8 -5
  132. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +5 -2
  133. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -3
  134. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -0
  135. package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
  136. package/src/form/field_bindings/MapFieldBinding.tsx +14 -16
  137. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +15 -8
  138. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +3 -3
  139. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -2
  140. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  141. package/src/form/field_bindings/RepeatFieldBinding.tsx +13 -4
  142. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -2
  143. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +11 -6
  144. package/src/form/field_bindings/SwitchFieldBinding.tsx +1 -0
  145. package/src/form/field_bindings/TextFieldBinding.tsx +1 -0
  146. package/src/form/index.tsx +16 -32
  147. package/src/form/validation.ts +12 -6
  148. package/src/hooks/data/save.ts +25 -30
  149. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  150. package/src/hooks/data/useEntityFetch.tsx +5 -1
  151. package/src/hooks/useBreadcrumbsController.tsx +31 -0
  152. package/src/hooks/useBuildModeController.tsx +15 -28
  153. package/src/hooks/useBuildNavigationController.tsx +81 -24
  154. package/src/hooks/useFireCMSContext.tsx +0 -30
  155. package/src/hooks/useLargeLayout.tsx +0 -35
  156. package/src/hooks/useModeController.tsx +1 -2
  157. package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
  158. package/src/internal/useBuildDataSource.ts +9 -5
  159. package/src/internal/useBuildSideDialogsController.tsx +3 -2
  160. package/src/internal/useBuildSideEntityController.tsx +135 -88
  161. package/src/internal/useUnsavedChangesDialog.tsx +126 -92
  162. package/src/preview/PropertyPreview.tsx +12 -10
  163. package/src/preview/components/UrlComponentPreview.tsx +17 -18
  164. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +4 -3
  165. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -3
  166. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +4 -2
  167. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +4 -3
  168. package/src/preview/property_previews/ArrayOneOfPreview.tsx +4 -2
  169. package/src/preview/property_previews/ArrayPropertyPreview.tsx +4 -2
  170. package/src/preview/property_previews/MapPropertyPreview.tsx +3 -2
  171. package/src/routes/CustomCMSRoute.tsx +21 -0
  172. package/src/routes/FireCMSRoute.tsx +246 -0
  173. package/src/routes/HomePageRoute.tsx +17 -0
  174. package/src/types/collections.ts +45 -13
  175. package/src/types/datasource.ts +2 -1
  176. package/src/types/dialogs_controller.tsx +7 -3
  177. package/src/types/entities.ts +1 -1
  178. package/src/types/entity_actions.tsx +12 -0
  179. package/src/types/fields.tsx +18 -3
  180. package/src/types/firecms.tsx +1 -1
  181. package/src/types/navigation.ts +41 -16
  182. package/src/types/plugins.tsx +2 -0
  183. package/src/types/properties.ts +48 -4
  184. package/src/types/side_dialogs_controller.tsx +2 -0
  185. package/src/types/side_entity_controller.tsx +11 -1
  186. package/src/util/builders.ts +5 -3
  187. package/src/util/createFormexStub.tsx +62 -0
  188. package/src/util/entity_actions.ts +28 -0
  189. package/src/util/entity_cache.ts +204 -0
  190. package/src/util/index.ts +1 -0
  191. package/src/util/join_collections.ts +6 -1
  192. package/src/util/navigation_from_path.ts +18 -7
  193. package/src/util/navigation_utils.ts +115 -14
  194. package/src/util/objects.ts +9 -2
  195. package/src/util/parent_references_from_path.ts +3 -3
  196. package/src/util/property_utils.tsx +7 -3
  197. package/src/util/references.ts +8 -6
  198. package/src/util/resolutions.ts +41 -12
  199. package/src/util/useStorageUploadController.tsx +60 -23
  200. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +0 -236
  201. /package/src/util/{common.tsx → common.ts} +0 -0
@@ -0,0 +1,782 @@
1
+ import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ AuthController,
4
+ CMSAnalyticsEvent,
5
+ Entity,
6
+ EntityCollection,
7
+ EntityCustomViewParams,
8
+ EntityStatus,
9
+ EntityValues,
10
+ FormContext,
11
+ PluginFormActionProps,
12
+ PropertyConfig,
13
+ PropertyFieldBindingProps,
14
+ ResolvedEntityCollection
15
+ } from "../types";
16
+ import equal from "react-fast-compare";
17
+
18
+ import { ErrorBoundary, getFormFieldKeys } from "../components";
19
+ import {
20
+ getDefaultValuesFor,
21
+ getEntityTitlePropertyKey,
22
+ getValueInPath,
23
+ isHidden,
24
+ isReadOnly,
25
+ mergeDeep,
26
+ resolveCollection,
27
+ useDebouncedCallback
28
+ } from "../util";
29
+
30
+ import {
31
+ saveEntityWithCallbacks,
32
+ useAuthController,
33
+ useCustomizationController,
34
+ useDataSource,
35
+ useFireCMSContext,
36
+ useSnackbarController
37
+ } from "../hooks";
38
+ import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
39
+ import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
40
+ import { useAnalyticsController } from "../hooks/useAnalyticsController";
41
+ import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
42
+ import { ValidationError } from "yup";
43
+ import { removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
44
+ import { CustomIdField } from "../form/components/CustomIdField";
45
+ import { ErrorFocus } from "../form/components/ErrorFocus";
46
+ import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
47
+ import { EntityFormActions, EntityFormActionsProps } from "./EntityFormActions";
48
+
49
+ export type OnUpdateParams = {
50
+ entity: Entity<any>,
51
+ status: EntityStatus,
52
+ path: string,
53
+ entityId?: string;
54
+ selectedTab?: string;
55
+ collection: EntityCollection<any>
56
+ };
57
+
58
+ export type EntityFormProps<M extends Record<string, any>> = {
59
+ path: string;
60
+ collection: EntityCollection<M>;
61
+ entityId?: string;
62
+ entity?: Entity<M>;
63
+ databaseId?: string;
64
+ onIdChange?: (id: string) => void;
65
+ onValuesModified?: (modified: boolean) => void;
66
+ onSaved?: (params: OnUpdateParams) => void;
67
+ initialDirtyValues?: Partial<M>; // dirty cached entity in memory
68
+ onFormContextReady?: (formContext: FormContext) => void;
69
+ forceActionsAtTheBottom?: boolean;
70
+ className?: string;
71
+ initialStatus: EntityStatus;
72
+ onStatusChange?: (status: EntityStatus) => void;
73
+ onEntityChange?: (entity: Entity<M>) => void;
74
+ formex?: FormexController<M>;
75
+ openEntityMode?: "side_panel" | "full_screen";
76
+ /**
77
+ * If true, the form will be disabled and no actions will be available
78
+ */
79
+ disabled?: boolean;
80
+ /**
81
+ * Include the copy and delete actions in the form
82
+ */
83
+ showDefaultActions?: boolean;
84
+
85
+ /**
86
+ * Display the entity path in the form
87
+ */
88
+ showEntityPath?: boolean;
89
+
90
+ EntityFormActionsComponent?: React.FC<EntityFormActionsProps>;
91
+
92
+ Builder?: React.ComponentType<EntityCustomViewParams<M>>;
93
+
94
+ children?: React.ReactNode;
95
+ };
96
+
97
+ export function EntityForm<M extends Record<string, any>>({
98
+ path,
99
+ entityId: entityIdProp,
100
+ collection,
101
+ onValuesModified,
102
+ onIdChange,
103
+ onSaved,
104
+ entity,
105
+ initialDirtyValues,
106
+ onFormContextReady,
107
+ forceActionsAtTheBottom,
108
+ initialStatus,
109
+ className,
110
+ onStatusChange,
111
+ onEntityChange,
112
+ openEntityMode = "full_screen",
113
+ formex: formexProp,
114
+ disabled: disabledProp,
115
+ Builder,
116
+ EntityFormActionsComponent = EntityFormActions,
117
+ showDefaultActions = true,
118
+ showEntityPath = true,
119
+ children
120
+ }: EntityFormProps<M>) {
121
+
122
+ if (collection.customId && collection.formAutoSave) {
123
+ console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
124
+ }
125
+
126
+ const authController = useAuthController();
127
+ const [status, setStatus] = useState<EntityStatus>(initialStatus);
128
+
129
+ const updateStatus = (status: EntityStatus) => {
130
+ setStatus(status);
131
+ onStatusChange?.(status);
132
+ };
133
+
134
+ const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
135
+ useDebouncedCallback(valuesToBeSaved, () => {
136
+ if (valuesToBeSaved)
137
+ saveEntity({
138
+ entityId: entityIdProp,
139
+ collection,
140
+ path,
141
+ values: valuesToBeSaved
142
+ });
143
+ }, false, 2000);
144
+
145
+ const dataSource = useDataSource(collection);
146
+ const snackbarController = useSnackbarController();
147
+ const customizationController = useCustomizationController();
148
+ const context = useFireCMSContext();
149
+ const analyticsController = useAnalyticsController();
150
+
151
+ const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
152
+
153
+ const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
154
+
155
+ const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
156
+ (Boolean(collection.customId) && collection.customId !== "optional");
157
+
158
+ const initialEntityId: string | undefined = useMemo((): string | undefined => {
159
+ if (status === "new" || status === "copy") {
160
+ if (mustSetCustomId) {
161
+ return undefined;
162
+ } else {
163
+ return dataSource.generateEntityId(path, collection);
164
+ }
165
+ } else {
166
+ return entityIdProp;
167
+ }
168
+ }, [entityIdProp, status]);
169
+
170
+ const [entityId, setEntityId] = useState<string | undefined>(initialEntityId);
171
+ const [entityIdError, setEntityIdError] = useState<boolean>(false);
172
+ const [savingError, setSavingError] = useState<Error | undefined>();
173
+
174
+ const autoSave = collection.formAutoSave && !collection.customId;
175
+
176
+ const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
177
+
178
+ if (mustSetCustomId && !entityId) {
179
+ console.error("Missing custom Id");
180
+ setEntityIdError(true);
181
+ formexController.setSubmitting(false);
182
+ return;
183
+ }
184
+
185
+ setSavingError(undefined);
186
+ setEntityIdError(false);
187
+
188
+ if (status === "existing") {
189
+ if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
190
+ } else if (status === "new" || status === "copy") {
191
+ if (collection.customId) {
192
+ if (collection.customId !== "optional" && !entityId) {
193
+ throw Error("Form misconfiguration when saving, entityId should be set");
194
+ }
195
+ }
196
+ } else {
197
+ throw Error("New FormType added, check EntityForm");
198
+ }
199
+
200
+ return save(values)
201
+ ?.then(_ => {
202
+ formexController.resetForm({
203
+ values,
204
+ submitCount: 0,
205
+ touched: {}
206
+ });
207
+ })
208
+ .finally(() => {
209
+ formexController.setSubmitting(false);
210
+ });
211
+ };
212
+
213
+ const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
214
+ initialValues: (initialDirtyValues ?? getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs)) as M,
215
+ initialDirty: Boolean(initialDirtyValues),
216
+ onSubmit,
217
+ onReset: () => {
218
+ clearDirtyCache();
219
+ onValuesModified?.(false);
220
+ },
221
+ validation: (values) => {
222
+ return validationSchema?.validate(values, { abortEarly: false })
223
+ .then(() => {
224
+ return {};
225
+ })
226
+ .catch((e: any) => {
227
+ return yupToFormErrors(e);
228
+ });
229
+ }
230
+ });
231
+
232
+ useEffect(() => {
233
+
234
+ const handleKeyDown = (e: KeyboardEvent) => {
235
+ const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z";
236
+ const isRedo =
237
+ ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
238
+ ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "y");
239
+
240
+ if (isUndo && formex.canUndo) {
241
+ e.preventDefault();
242
+ formex.undo();
243
+ } else if (isRedo && formex.canRedo) {
244
+ e.preventDefault();
245
+ formex.redo();
246
+ }
247
+ };
248
+
249
+ window.addEventListener("keydown", handleKeyDown);
250
+ return () => window.removeEventListener("keydown", handleKeyDown);
251
+
252
+ }, [formex]);
253
+
254
+ const resolvedCollection = useMemo(() => resolveCollection<M>({
255
+ collection,
256
+ path,
257
+ entityId,
258
+ values: formex.values,
259
+ previousValues: formex.initialValues,
260
+ propertyConfigs: customizationController.propertyConfigs,
261
+ authController
262
+ }), [collection, path, entityId, formex.values, formex.initialValues, customizationController.propertyConfigs]);
263
+
264
+ const onPreSaveHookError = useCallback((e: Error) => {
265
+ snackbarController.open({
266
+ type: "error",
267
+ message: "Error before saving: " + e?.message
268
+ });
269
+ console.error(e);
270
+ }, [snackbarController]);
271
+
272
+ const onSaveSuccessHookError = useCallback((e: Error) => {
273
+ snackbarController.open({
274
+ type: "error",
275
+ message: "Error after saving (entity is saved): " + e?.message
276
+ });
277
+ console.error(e);
278
+ }, [snackbarController]);
279
+
280
+ function clearDirtyCache() {
281
+ if (status === "new" || status === "copy") {
282
+ removeEntityFromCache(path + "#new");
283
+ } else {
284
+ removeEntityFromCache(path + "/" + entityId);
285
+ }
286
+ }
287
+
288
+ const onSaveSuccess = (updatedEntity: Entity<M>) => {
289
+
290
+ clearDirtyCache();
291
+ onValuesModified?.(false);
292
+ if (!autoSave)
293
+ snackbarController.open({
294
+ type: "success",
295
+ message: `${collection.singularName ?? collection.name}: Saved correctly`
296
+ });
297
+ onEntityChange?.(updatedEntity);
298
+ updateStatus("existing");
299
+ setEntityId(updatedEntity.id);
300
+
301
+ if (onSaved) {
302
+ onSaved({
303
+ entity: updatedEntity,
304
+ status,
305
+ path,
306
+ entityId: updatedEntity.id,
307
+ collection
308
+ });
309
+ }
310
+ };
311
+
312
+ const onSaveFailure = useCallback((e: Error) => {
313
+ snackbarController.open({
314
+ type: "error",
315
+ message: "Error saving: " + e?.message
316
+ });
317
+ console.error("Error saving entity", path, entityId);
318
+ console.error(e);
319
+ }, [entityId, path, snackbarController]);
320
+
321
+ const saveEntity = ({
322
+ values,
323
+ previousValues,
324
+ entityId,
325
+ collection,
326
+ path
327
+ }: {
328
+ collection: EntityCollection<M>,
329
+ path: string,
330
+ entityId: string | undefined,
331
+ values: M,
332
+ previousValues?: M,
333
+ }) => {
334
+ return saveEntityWithCallbacks({
335
+ path,
336
+ entityId,
337
+ values,
338
+ previousValues,
339
+ collection,
340
+ status,
341
+ dataSource,
342
+ context,
343
+ onSaveSuccess,
344
+ onSaveFailure,
345
+ onPreSaveHookError,
346
+ onSaveSuccessHookError
347
+ }).then();
348
+ };
349
+
350
+ type EntityFormSaveParams<M extends Record<string, any>> = {
351
+ collection: ResolvedEntityCollection<M>,
352
+ path: string,
353
+ entityId: string | undefined,
354
+ values: EntityValues<M>,
355
+ previousValues?: EntityValues<M>,
356
+ autoSave: boolean
357
+ };
358
+
359
+ const onSaveEntityRequest = async ({
360
+ collection,
361
+ path,
362
+ entityId,
363
+ values,
364
+ previousValues,
365
+ autoSave
366
+ }: EntityFormSaveParams<M>): Promise<void> => {
367
+ if (!status)
368
+ return;
369
+ if (autoSave) {
370
+ setValuesToBeSaved(values);
371
+ } else {
372
+ return saveEntity({
373
+ collection,
374
+ path,
375
+ entityId,
376
+ values,
377
+ previousValues
378
+ });
379
+ }
380
+ };
381
+
382
+ const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
383
+ const save = (values: EntityValues<M>): Promise<void> => {
384
+ lastSavedValues.current = values;
385
+ return onSaveEntityRequest({
386
+ collection: resolvedCollection,
387
+ path,
388
+ entityId,
389
+ values,
390
+ previousValues: entity?.values,
391
+ autoSave: autoSave ?? false
392
+ }).then((res) => {
393
+ const eventName: CMSAnalyticsEvent = status === "new"
394
+ ? "new_entity_saved"
395
+ : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
396
+ analyticsController.onAnalyticsEvent?.(eventName, { path });
397
+ }).catch(e => {
398
+ console.error(e);
399
+ setSavingError(e);
400
+ });
401
+ };
402
+
403
+ const disabled = formex.isSubmitting || Boolean(disabledProp);
404
+
405
+ const formContext: FormContext<M> = {
406
+ // @ts-ignore
407
+ setFieldValue: useCallback(formex.setFieldValue, []),
408
+ values: formex.values,
409
+ collection: resolvedCollection,
410
+ entityId: entityId as string,
411
+ path,
412
+ save,
413
+ formex,
414
+ entity,
415
+ savingError,
416
+ status,
417
+ openEntityMode,
418
+ disabled
419
+ };
420
+
421
+ useEffect(() => {
422
+ onFormContextReady?.(formContext);
423
+ }, [formex.version, resolvedCollection, entityId, path]);
424
+
425
+ const onIdUpdateError = useCallback((error: any) => {
426
+ snackbarController.open({
427
+ type: "error",
428
+ message: "Error updating id, check the console"
429
+ });
430
+ }, []);
431
+
432
+ const pluginActions: React.ReactNode[] = [];
433
+ const plugins = customizationController.plugins;
434
+
435
+ const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
436
+ if (plugins && collection) {
437
+ const actionProps: PluginFormActionProps = {
438
+ entityId,
439
+ path,
440
+ status,
441
+ collection: collection,
442
+ context,
443
+ currentEntityId: entityId,
444
+ formContext,
445
+ openEntityMode,
446
+ disabled: actionsDisabled,
447
+ };
448
+ pluginActions.push(...plugins.map((plugin) => (
449
+ plugin.form?.Actions
450
+ ? <plugin.form.Actions
451
+ key={`actions_${plugin.key}`} {...actionProps} />
452
+ : null
453
+ )).filter(Boolean));
454
+ }
455
+
456
+ const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
457
+ const title = (formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined)
458
+ ?? collection.singularName
459
+ ?? collection.name;
460
+
461
+ const onIdUpdate = collection.callbacks?.onIdUpdate;
462
+ const doOnIdUpdate = useCallback(async () => {
463
+ if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
464
+ setCustomIdLoading(true);
465
+ try {
466
+ const updatedId = await onIdUpdate({
467
+ collection: resolvedCollection,
468
+ path,
469
+ entityId,
470
+ values: formex.values,
471
+ context
472
+ });
473
+ setEntityId(updatedId);
474
+ } catch (e) {
475
+ onIdUpdateError?.(e);
476
+ console.error(e);
477
+ }
478
+ setCustomIdLoading(false);
479
+ }
480
+ }, [entityId, formex.values, status, onIdUpdate, resolvedCollection, path, context, onIdUpdateError]);
481
+
482
+ useEffect(() => {
483
+ doOnIdUpdate();
484
+ }, [doOnIdUpdate]);
485
+
486
+ useEffect(() => {
487
+ if (!autoSave) {
488
+ onValuesModified?.(modified);
489
+ }
490
+ }, [formex.dirty]);
491
+
492
+ const deferredValues = useDeferredValue(formex.values);
493
+ const modified = formex.dirty;
494
+
495
+ const uniqueFieldValidator: CustomFieldValidator = useCallback(({
496
+ name,
497
+ value,
498
+ property
499
+ }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
500
+ [dataSource, path, entityId]);
501
+
502
+ const validationSchema = useMemo(() => entityId
503
+ ? getYupEntitySchema(
504
+ entityId,
505
+ resolvedCollection.properties,
506
+ uniqueFieldValidator)
507
+ : undefined,
508
+ [entityId, resolvedCollection.properties, uniqueFieldValidator]);
509
+
510
+ useEffect(() => {
511
+ const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
512
+ if (modified) {
513
+ saveEntityToCache(key, deferredValues);
514
+ }
515
+ }, [deferredValues, modified, path, entityId, status]);
516
+
517
+ useOnAutoSave(autoSave, formex, lastSavedValues, save);
518
+
519
+ useEffect(() => {
520
+ if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
521
+ // we update the form fields from the Firestore data
522
+ // if they were not touched
523
+ Object.entries(underlyingChanges).forEach(([key, value]) => {
524
+ const formValue = formex.values[key];
525
+ if (!equal(value, formValue) && !formex.touched[key]) {
526
+ console.debug("Updated value from the datasource:", key, value);
527
+ formex.setFieldValue(key, value !== undefined ? value : null);
528
+ }
529
+ });
530
+ }
531
+ }, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
532
+
533
+ const formFieldKeys = getFormFieldKeys(resolvedCollection);
534
+
535
+ const formFields = () => {
536
+
537
+ if (Builder) {
538
+ return <Builder
539
+ collection={collection}
540
+ entity={entity}
541
+ modifiedValues={formex.values}
542
+ formContext={formContext}
543
+ />;
544
+ }
545
+ return (
546
+ <FormLayout>
547
+ {formFieldKeys.map((key) => {
548
+ const property = resolvedCollection.properties[key];
549
+ if (property) {
550
+ const underlyingValueHasChanged: boolean =
551
+ !!underlyingChanges &&
552
+ Object.keys(underlyingChanges).includes(key) &&
553
+ formex.touched[key];
554
+ const disabled = disabledProp || (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
555
+ const hidden = isHidden(property);
556
+ if (hidden) return null;
557
+ const widthPercentage = property.widthPercentage ?? 100;
558
+ const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
559
+ propertyKey: key,
560
+ disabled,
561
+ property,
562
+ includeDescription: property.description || property.longDescription,
563
+ underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
564
+ context: formContext,
565
+ partOfArray: false,
566
+ minimalistView: false,
567
+ autoFocus: false
568
+ };
569
+
570
+ return (
571
+ <FormEntry propertyKey={key}
572
+ widthPercentage={widthPercentage}
573
+ key={`field_${key}`}>
574
+ <PropertyFieldBinding {...cmsFormFieldProps} />
575
+ </FormEntry>
576
+ );
577
+ }
578
+
579
+ const additionalField = resolvedCollection.additionalFields?.find(f => f.key === key);
580
+ if (additionalField && entity) {
581
+ const Builder = additionalField.Builder;
582
+ if (!Builder && !additionalField.value) {
583
+ throw new Error("When using additional fields you need to provide a Builder or a value");
584
+ }
585
+ const child = Builder
586
+ ? <Builder entity={entity} context={context}/>
587
+ : <div className={"w-full"}>
588
+ {additionalField.value?.({
589
+ entity,
590
+ context
591
+ })?.toString()}
592
+ </div>;
593
+
594
+ return (
595
+ <div key={`additional_${key}`} className={"w-full"}>
596
+ <LabelWithIconAndTooltip
597
+ propertyKey={key}
598
+ icon={<NotesIcon size={"small"}/>}
599
+ title={additionalField.name}
600
+ className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
601
+ <div
602
+ className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
603
+ <ErrorBoundary>
604
+ {child}
605
+ </ErrorBoundary>
606
+ </div>
607
+ </div>
608
+ );
609
+ }
610
+
611
+ console.warn(`Property ${key} not found in collection ${resolvedCollection.name} in properties or additional fields. Skipping.`);
612
+ return null;
613
+ }).filter(Boolean)}
614
+ </FormLayout>
615
+ );
616
+ };
617
+
618
+ const formRef = useRef<HTMLDivElement>(null);
619
+
620
+ const formView = <ErrorBoundary>
621
+ <>
622
+ {!Builder && <div className={"w-full py-2 flex flex-col items-start my-4 lg:my-6"}>
623
+ <Typography
624
+ className={"py-4 flex-grow line-clamp-1 " + (collection.hideIdFromForm ? "mb-2" : "mb-0")}
625
+ variant={"h4"}>
626
+ {title ?? collection.singularName ?? collection.name}
627
+ </Typography>
628
+ {showEntityPath && <Alert color={"base"} className={"w-full"} size={"small"}>
629
+ <code
630
+ className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
631
+ {entity?.path ?? path}/{entityId}
632
+ </code>
633
+ </Alert>}
634
+ </div>}
635
+
636
+ {children}
637
+
638
+ {!Builder && !collection.hideIdFromForm &&
639
+ <CustomIdField customId={collection.customId}
640
+ entityId={entityId}
641
+ status={status}
642
+ onChange={setEntityId}
643
+ error={entityIdError}
644
+ loading={customIdLoading}
645
+ entity={entity}/>
646
+ }
647
+
648
+ {entityId && formContext && <>
649
+ <div className="mt-12 flex flex-col gap-8" ref={formRef}>
650
+ {formFields()}
651
+ <ErrorFocus containerRef={formRef}/>
652
+ </div>
653
+ </>}
654
+
655
+ {forceActionsAtTheBottom && <div className="h-16"/>}
656
+ </>
657
+ </ErrorBoundary>;
658
+
659
+ useEffect(() => {
660
+ if (entityId && onIdChange)
661
+ onIdChange(entityId);
662
+ }, [entityId, onIdChange]);
663
+
664
+ if (!resolvedCollection || !path) {
665
+ throw Error("INTERNAL: Collection and path must be defined in form context");
666
+ }
667
+
668
+ const dialogActions = <EntityFormActionsComponent
669
+ collection={resolvedCollection}
670
+ path={path}
671
+ entity={entity}
672
+ layout={forceActionsAtTheBottom ? "bottom" : "side"}
673
+ savingError={savingError}
674
+ formex={formex}
675
+ disabled={actionsDisabled}
676
+ status={status}
677
+ pluginActions={pluginActions ?? []}
678
+ openEntityMode={openEntityMode}
679
+ showDefaultActions={showDefaultActions}
680
+ />;
681
+
682
+ return (
683
+ <Formex value={formex}>
684
+ <form
685
+ onSubmit={formex.handleSubmit}
686
+ onReset={() => formex.resetForm({
687
+ values: getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs) as M
688
+ })}
689
+ noValidate
690
+ className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
691
+ <div
692
+ id={`form_${path}`}
693
+ className={cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit")}>
694
+
695
+ <div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
696
+
697
+ {formex.dirty
698
+ ? <Tooltip title={"Local unsaved changes"}
699
+ className={"self-end sticky top-4 z-10"}>
700
+ <Chip size={"small"} colorScheme={"orangeDarker"}>
701
+ <EditIcon size={"smallest"}/>
702
+ </Chip>
703
+ </Tooltip>
704
+ : <Tooltip title={"In sync with the database"}
705
+ className={"self-end sticky top-4 z-10"}>
706
+ <Chip size={"small"}>
707
+ <CheckIcon size={"smallest"}/>
708
+ </Chip>
709
+ </Tooltip>}
710
+
711
+ {formView}
712
+
713
+ </div>
714
+
715
+ </div>
716
+ {dialogActions}
717
+ </form>
718
+
719
+ </Formex>
720
+ );
721
+ }
722
+
723
+ function getInitialEntityValues<M extends object>(
724
+ authController: AuthController,
725
+ collection: EntityCollection,
726
+ path: string,
727
+ status: "new" | "existing" | "copy",
728
+ entity: Entity<M> | undefined,
729
+ propertyConfigs?: Record<string, PropertyConfig>,
730
+ ): Partial<EntityValues<M>> {
731
+ const resolvedCollection = resolveCollection({
732
+ collection,
733
+ path,
734
+ values: entity?.values,
735
+ propertyConfigs,
736
+ authController
737
+ });
738
+ const properties = resolvedCollection.properties;
739
+ if ((status === "existing" || status === "copy") && entity) {
740
+ if (!collection.alwaysApplyDefaultValues) {
741
+ return entity.values ?? getDefaultValuesFor(properties);
742
+ } else {
743
+ const defaultValues = getDefaultValuesFor(properties);
744
+ return mergeDeep(defaultValues, entity.values ?? {});
745
+ }
746
+ } else if (status === "new") {
747
+ return getDefaultValuesFor(properties);
748
+ } else {
749
+ console.error({
750
+ status,
751
+ entity
752
+ });
753
+ throw new Error("Form has not been initialised with the correct parameters");
754
+ }
755
+ }
756
+
757
+ export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
758
+ let errors: Record<string, any> = {};
759
+ if (yupError.inner) {
760
+ if (yupError.inner.length === 0) {
761
+ return setIn(errors, yupError.path!, yupError.message);
762
+ }
763
+ for (const err of yupError.inner) {
764
+ if (!getIn(errors, err.path!)) {
765
+ errors = setIn(errors, err.path!, err.message);
766
+ }
767
+ }
768
+ }
769
+ return errors;
770
+ }
771
+
772
+ function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: any, save: (values: EntityValues<any>) => Promise<void>) {
773
+ if (!autoSave) return;
774
+ useEffect(() => {
775
+ if (autoSave) {
776
+ if (formex.values && !equal(formex.values, lastSavedValues.current)) {
777
+ save(formex.values);
778
+ }
779
+ }
780
+ }, [formex.values]);
781
+ }
782
+