@firecms/core 3.0.0-beta.10 → 3.0.0-beta.12

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