@firecms/core 3.0.0-canary.7 → 3.0.0-canary.70

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 (271) hide show
  1. package/README.md +2 -2
  2. package/dist/app/AppBar.d.ts +12 -0
  3. package/dist/app/Drawer.d.ts +17 -0
  4. package/dist/app/Scaffold.d.ts +30 -0
  5. package/dist/app/index.d.ts +4 -0
  6. package/dist/app/useApp.d.ts +16 -0
  7. package/dist/components/ClearFilterSortButton.d.ts +5 -0
  8. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +11 -11
  9. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  10. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +5 -3
  11. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +3 -2
  12. package/dist/components/EntityCollectionTable/column_utils.d.ts +1 -2
  13. package/dist/components/EntityCollectionTable/fields/TableReferenceField.d.ts +2 -0
  14. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
  15. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +2 -2
  16. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +1 -1
  17. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +12 -3
  18. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
  19. package/dist/components/EntityCollectionView/useSelectionController.d.ts +2 -0
  20. package/dist/components/EntityPreview.d.ts +26 -7
  21. package/dist/components/EntityView.d.ts +11 -0
  22. package/dist/components/FieldCaption.d.ts +5 -0
  23. package/dist/components/HomePage/NavigationCard.d.ts +8 -0
  24. package/dist/components/HomePage/{NavigationCollectionCard.d.ts → NavigationCardBinding.d.ts} +2 -2
  25. package/dist/components/HomePage/SmallNavigationCard.d.ts +6 -0
  26. package/dist/components/HomePage/index.d.ts +3 -1
  27. package/dist/components/ReferenceWidget.d.ts +3 -1
  28. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  29. package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +2 -1
  30. package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -7
  31. package/dist/components/VirtualTable/types.d.ts +3 -3
  32. package/dist/components/{EntityCollectionTable/internal → common}/default_entity_actions.d.ts +1 -1
  33. package/dist/components/common/index.d.ts +1 -0
  34. package/dist/components/common/table_height.d.ts +5 -0
  35. package/dist/components/common/types.d.ts +4 -6
  36. package/dist/components/common/useDataSourceEntityCollectionTableController.d.ts +3 -0
  37. package/dist/components/index.d.ts +6 -3
  38. package/dist/contexts/AuthControllerContext.d.ts +1 -1
  39. package/dist/{components/FireCMSAppBar.d.ts → core/DefaultAppBar.d.ts} +5 -8
  40. package/dist/core/DefaultDrawer.d.ts +19 -0
  41. package/dist/core/DrawerNavigationItem.d.ts +9 -0
  42. package/dist/core/EntityEditView.d.ts +36 -0
  43. package/dist/core/NavigationRoutes.d.ts +2 -2
  44. package/dist/core/index.d.ts +3 -4
  45. package/dist/form/PropertiesForm.d.ts +8 -0
  46. package/dist/form/components/ErrorFocus.d.ts +1 -1
  47. package/dist/form/components/FieldHelperText.d.ts +3 -3
  48. package/dist/form/components/StorageItemPreview.d.ts +2 -3
  49. package/dist/form/components/StorageUploadProgress.d.ts +1 -1
  50. package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
  51. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  52. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -4
  53. package/dist/form/field_bindings/TextFieldBinding.d.ts +2 -2
  54. package/dist/form/index.d.ts +0 -2
  55. package/dist/form/validation.d.ts +1 -1
  56. package/dist/hooks/data/delete.d.ts +2 -2
  57. package/dist/hooks/data/save.d.ts +2 -3
  58. package/dist/hooks/data/useDataSource.d.ts +2 -2
  59. package/dist/hooks/data/useEntityFetch.d.ts +3 -3
  60. package/dist/hooks/index.d.ts +2 -0
  61. package/dist/hooks/useBuildNavigationController.d.ts +6 -4
  62. package/dist/hooks/useProjectLog.d.ts +6 -2
  63. package/dist/hooks/useStorageSource.d.ts +2 -2
  64. package/dist/hooks/useValidateAuthenticator.d.ts +21 -0
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.es.js +10512 -9997
  67. package/dist/index.es.js.map +1 -1
  68. package/dist/index.umd.js +5 -5
  69. package/dist/index.umd.js.map +1 -1
  70. package/dist/internal/useBuildDataSource.d.ts +1 -16
  71. package/dist/preview/PropertyPreview.d.ts +1 -1
  72. package/dist/preview/PropertyPreviewProps.d.ts +1 -4
  73. package/dist/preview/components/BooleanPreview.d.ts +5 -1
  74. package/dist/preview/components/EnumValuesChip.d.ts +1 -1
  75. package/dist/preview/components/ReferencePreview.d.ts +3 -8
  76. package/dist/types/analytics.d.ts +1 -1
  77. package/dist/types/auth.d.ts +37 -1
  78. package/dist/types/collections.d.ts +44 -6
  79. package/dist/types/datasource.d.ts +21 -14
  80. package/dist/types/entities.d.ts +5 -1
  81. package/dist/types/entity_actions.d.ts +14 -0
  82. package/dist/types/entity_callbacks.d.ts +2 -2
  83. package/dist/types/entity_overrides.d.ts +6 -0
  84. package/dist/types/fields.d.ts +31 -30
  85. package/dist/types/index.d.ts +2 -1
  86. package/dist/types/navigation.d.ts +15 -14
  87. package/dist/types/permissions.d.ts +5 -1
  88. package/dist/types/plugins.d.ts +22 -22
  89. package/dist/types/properties.d.ts +13 -5
  90. package/dist/types/property_config.d.ts +2 -2
  91. package/dist/types/roles.d.ts +31 -0
  92. package/dist/types/storage.d.ts +11 -3
  93. package/dist/types/user.d.ts +5 -0
  94. package/dist/util/collections.d.ts +9 -1
  95. package/dist/util/entities.d.ts +1 -1
  96. package/dist/util/icon_synonyms.d.ts +1 -97
  97. package/dist/util/icons.d.ts +8 -2
  98. package/dist/util/navigation_utils.d.ts +2 -2
  99. package/dist/util/objects.d.ts +1 -1
  100. package/dist/util/permissions.d.ts +4 -4
  101. package/dist/util/references.d.ts +4 -2
  102. package/dist/util/resolutions.d.ts +14 -14
  103. package/dist/util/storage.d.ts +23 -2
  104. package/dist/util/useStorageUploadController.d.ts +1 -1
  105. package/dist/util/useTraceUpdate.d.ts +1 -0
  106. package/package.json +139 -119
  107. package/src/app/AppBar.tsx +18 -0
  108. package/src/app/Drawer.tsx +25 -0
  109. package/src/app/Scaffold.tsx +249 -0
  110. package/src/app/index.ts +4 -0
  111. package/src/app/useApp.tsx +32 -0
  112. package/src/components/ClearFilterSortButton.tsx +41 -0
  113. package/src/components/DeleteEntityDialog.tsx +4 -4
  114. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +4 -4
  115. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +276 -279
  116. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +9 -5
  117. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +48 -45
  118. package/src/components/EntityCollectionTable/column_utils.tsx +3 -3
  119. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +18 -17
  120. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +5 -5
  121. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +29 -34
  122. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +16 -12
  123. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +4 -5
  124. package/src/components/EntityCollectionView/EntityCollectionView.tsx +73 -72
  125. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
  126. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
  127. package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
  128. package/src/components/EntityPreview.tsx +209 -70
  129. package/src/components/EntityView.tsx +84 -0
  130. package/src/components/FieldCaption.tsx +14 -0
  131. package/src/components/HomePage/DefaultHomePage.tsx +15 -11
  132. package/src/components/HomePage/NavigationCard.tsx +69 -0
  133. package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
  134. package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
  135. package/src/components/HomePage/index.tsx +3 -1
  136. package/src/components/PropertyIdCopyTooltipContent.tsx +2 -3
  137. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +4 -4
  138. package/src/components/ReferenceWidget.tsx +22 -12
  139. package/src/components/SearchIconsView.tsx +5 -5
  140. package/src/components/SelectableTable/SelectableTable.tsx +5 -3
  141. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +2 -3
  142. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +23 -8
  143. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -24
  144. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
  145. package/src/components/VirtualTable/VirtualTable.tsx +38 -29
  146. package/src/components/VirtualTable/VirtualTableHeader.tsx +4 -4
  147. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +2 -2
  148. package/src/components/VirtualTable/VirtualTableProps.tsx +7 -7
  149. package/src/components/VirtualTable/VirtualTableRow.tsx +4 -5
  150. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +1 -1
  151. package/src/components/VirtualTable/types.tsx +2 -3
  152. package/src/components/{EntityCollectionTable/internal → common}/default_entity_actions.tsx +11 -7
  153. package/src/components/common/index.ts +1 -0
  154. package/src/components/{VirtualTable/common.tsx → common/table_height.tsx} +5 -2
  155. package/src/components/common/types.tsx +4 -6
  156. package/src/components/common/useColumnsIds.tsx +10 -2
  157. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +12 -1
  158. package/src/components/common/useTableSearchHelper.ts +39 -9
  159. package/src/components/index.tsx +6 -3
  160. package/src/contexts/AuthControllerContext.tsx +1 -1
  161. package/src/{components/FireCMSAppBar.tsx → core/DefaultAppBar.tsx} +51 -34
  162. package/src/core/DefaultDrawer.tsx +177 -0
  163. package/src/core/DrawerNavigationItem.tsx +62 -0
  164. package/src/core/EntityEditView.tsx +1101 -0
  165. package/src/core/EntitySidePanel.tsx +3 -4
  166. package/src/core/FireCMS.tsx +54 -43
  167. package/src/core/NavigationRoutes.tsx +14 -7
  168. package/src/core/field_configs.tsx +2 -3
  169. package/src/core/index.tsx +3 -4
  170. package/src/form/PropertiesForm.tsx +81 -0
  171. package/src/form/PropertyFieldBinding.tsx +29 -7
  172. package/src/form/components/FieldHelperText.tsx +3 -3
  173. package/src/form/components/StorageItemPreview.tsx +5 -7
  174. package/src/form/components/StorageUploadProgress.tsx +9 -8
  175. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +10 -12
  176. package/src/form/field_bindings/BlockFieldBinding.tsx +2 -2
  177. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
  178. package/src/form/field_bindings/KeyValueFieldBinding.tsx +19 -19
  179. package/src/form/field_bindings/MapFieldBinding.tsx +25 -17
  180. package/src/form/field_bindings/MarkdownFieldBinding.tsx +2 -2
  181. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -9
  182. package/src/form/field_bindings/ReferenceFieldBinding.tsx +16 -13
  183. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -3
  184. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +14 -35
  185. package/src/form/field_bindings/TextFieldBinding.tsx +7 -5
  186. package/src/form/index.tsx +4 -4
  187. package/src/form/validation.ts +4 -21
  188. package/src/hooks/data/delete.ts +3 -3
  189. package/src/hooks/data/save.ts +4 -2
  190. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  191. package/src/hooks/data/useDataSource.tsx +8 -3
  192. package/src/hooks/data/useEntityFetch.tsx +4 -4
  193. package/src/hooks/index.tsx +3 -0
  194. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +8 -10
  195. package/src/hooks/useBuildModeController.tsx +11 -5
  196. package/src/hooks/useBuildNavigationController.tsx +200 -83
  197. package/src/hooks/useProjectLog.tsx +17 -7
  198. package/src/hooks/useReferenceDialog.tsx +2 -2
  199. package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
  200. package/src/hooks/useStorageSource.tsx +7 -2
  201. package/src/hooks/useValidateAuthenticator.tsx +115 -0
  202. package/src/index.ts +1 -0
  203. package/src/internal/useBuildDataSource.ts +54 -47
  204. package/src/internal/useBuildSideEntityController.tsx +88 -21
  205. package/src/preview/PropertyPreview.tsx +5 -15
  206. package/src/preview/PropertyPreviewProps.tsx +1 -11
  207. package/src/preview/components/BooleanPreview.tsx +19 -4
  208. package/src/preview/components/EnumValuesChip.tsx +2 -2
  209. package/src/preview/components/ReferencePreview.tsx +72 -165
  210. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +0 -1
  211. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +2 -1
  212. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +0 -1
  213. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +0 -1
  214. package/src/preview/property_previews/ArrayOneOfPreview.tsx +2 -3
  215. package/src/preview/property_previews/ArrayPropertyPreview.tsx +2 -3
  216. package/src/preview/property_previews/MapPropertyPreview.tsx +5 -5
  217. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
  218. package/src/types/analytics.ts +1 -0
  219. package/src/types/auth.tsx +50 -1
  220. package/src/types/collections.ts +51 -6
  221. package/src/types/customization_controller.tsx +0 -1
  222. package/src/types/datasource.ts +24 -17
  223. package/src/types/entities.ts +9 -1
  224. package/src/types/entity_actions.tsx +17 -0
  225. package/src/types/entity_callbacks.ts +2 -2
  226. package/src/types/entity_overrides.tsx +7 -0
  227. package/src/types/fields.tsx +33 -33
  228. package/src/types/firecms.tsx +0 -1
  229. package/src/types/index.ts +2 -1
  230. package/src/types/navigation.ts +17 -17
  231. package/src/types/permissions.ts +6 -1
  232. package/src/types/plugins.tsx +28 -30
  233. package/src/types/properties.ts +19 -7
  234. package/src/types/property_config.tsx +2 -2
  235. package/src/types/roles.ts +41 -0
  236. package/src/types/side_entity_controller.tsx +1 -0
  237. package/src/types/storage.ts +12 -3
  238. package/src/types/user.ts +7 -0
  239. package/src/util/collections.ts +22 -0
  240. package/src/util/entities.ts +2 -1
  241. package/src/util/enums.ts +1 -1
  242. package/src/util/icon_list.ts +2 -2
  243. package/src/util/icon_synonyms.ts +3 -99
  244. package/src/util/icons.tsx +11 -3
  245. package/src/util/navigation_utils.ts +6 -6
  246. package/src/util/objects.ts +8 -21
  247. package/src/util/permissions.ts +12 -8
  248. package/src/util/references.ts +36 -5
  249. package/src/util/resolutions.ts +32 -31
  250. package/src/util/storage.ts +75 -21
  251. package/src/util/strings.ts +2 -2
  252. package/src/util/useStorageUploadController.tsx +21 -3
  253. package/src/util/useTraceUpdate.tsx +2 -1
  254. package/dist/components/VirtualTable/common.d.ts +0 -2
  255. package/dist/core/Drawer.d.ts +0 -23
  256. package/dist/core/EntityView.d.ts +0 -22
  257. package/dist/core/Scaffold.d.ts +0 -55
  258. package/dist/core/SideEntityView.d.ts +0 -7
  259. package/dist/form/EntityForm.d.ts +0 -77
  260. package/dist/internal/useBuildCustomizationController.d.ts +0 -2
  261. package/dist/internal/useLocaleConfig.d.ts +0 -1
  262. package/dist/types/appcheck.d.ts +0 -26
  263. package/src/components/HomePage/NavigationCollectionCard.tsx +0 -146
  264. package/src/core/Drawer.tsx +0 -164
  265. package/src/core/EntityView.tsx +0 -578
  266. package/src/core/Scaffold.tsx +0 -281
  267. package/src/core/SideEntityView.tsx +0 -38
  268. package/src/form/EntityForm.tsx +0 -720
  269. package/src/internal/useBuildCustomizationController.tsx +0 -5
  270. package/src/internal/useLocaleConfig.tsx +0 -18
  271. package/src/types/appcheck.ts +0 -29
@@ -0,0 +1,1101 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ CMSAnalyticsEvent,
4
+ Entity,
5
+ EntityAction,
6
+ EntityCollection,
7
+ EntityCustomView,
8
+ EntityStatus,
9
+ EntityValues,
10
+ FireCMSPlugin,
11
+ FormContext,
12
+ PluginFormActionProps,
13
+ PropertyFieldBindingProps,
14
+ ResolvedEntityCollection,
15
+ User
16
+ } from "../types";
17
+ import equal from "react-fast-compare"
18
+
19
+ import {
20
+ CircularProgressCenter,
21
+ copyEntityAction,
22
+ deleteEntityAction,
23
+ EntityCollectionView,
24
+ EntityView,
25
+ ErrorBoundary,
26
+ } from "../components";
27
+ import {
28
+ canCreateEntity,
29
+ canDeleteEntity,
30
+ canEditEntity,
31
+ getDefaultValuesFor,
32
+ getEntityTitlePropertyKey,
33
+ getValueInPath,
34
+ isHidden,
35
+ isReadOnly,
36
+ removeInitialAndTrailingSlashes,
37
+ resolveCollection,
38
+ resolveDefaultSelectedView,
39
+ resolveEntityView,
40
+ useDebouncedCallback
41
+ } from "../util";
42
+
43
+ import {
44
+ saveEntityWithCallbacks,
45
+ useAuthController,
46
+ useCustomizationController,
47
+ useDataSource,
48
+ useEntityFetch,
49
+ useFireCMSContext,
50
+ useSideEntityController,
51
+ useSnackbarController
52
+ } from "../hooks";
53
+ import {
54
+ Alert,
55
+ Button,
56
+ CircularProgress,
57
+ CloseIcon,
58
+ cls,
59
+ defaultBorderMixin,
60
+ DialogActions,
61
+ IconButton,
62
+ Tab,
63
+ Tabs,
64
+ Tooltip,
65
+ Typography
66
+ } from "@firecms/ui";
67
+ import { useSideDialogContext } from "./index";
68
+ import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
69
+ import { useAnalyticsController } from "../hooks/useAnalyticsController";
70
+ import { CustomIdField } from "../form/components/CustomIdField";
71
+ import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
72
+ import { ErrorFocus } from "../form/components/ErrorFocus";
73
+ import { PropertyIdCopyTooltipContent } from "../components/PropertyIdCopyTooltipContent";
74
+ import { PropertyFieldBinding } from "../form";
75
+ import { ValidationError } from "yup";
76
+
77
+ const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
78
+
79
+ export interface EntityEditViewProps<M extends Record<string, any>> {
80
+ path: string;
81
+ collection: EntityCollection<M>;
82
+ entityId?: string;
83
+ copy?: boolean;
84
+ selectedSubPath?: string;
85
+ parentCollectionIds: string[];
86
+ onValuesAreModified: (modified: boolean) => void;
87
+ onUpdate?: (params: { entity: Entity<any> }) => void;
88
+ onClose?: () => void;
89
+ }
90
+
91
+ /**
92
+ * This is the default view that is used as the content of a side panel when
93
+ * an entity is opened.
94
+ * You probably don't want to use this view directly since it is bound to the
95
+ * side panel. Instead, you might want to use {@link EntityForm} or {@link EntityCollectionView}
96
+ */
97
+ export function EntityEditView<M extends Record<string, any>, UserType extends User>({
98
+ entityId: entityIdProp,
99
+ ...props
100
+ }: EntityEditViewProps<M>) {
101
+ const {
102
+ entity,
103
+ dataLoading,
104
+ // eslint-disable-next-line no-unused-vars
105
+ dataLoadingError
106
+ } = useEntityFetch<M, UserType>({
107
+ path: props.path,
108
+ entityId: entityIdProp,
109
+ collection: props.collection,
110
+ useCache: false
111
+ });
112
+
113
+ if (dataLoading) {
114
+ return <CircularProgressCenter/>
115
+ }
116
+
117
+ return <EntityEditViewInner<M> {...props}
118
+ entityId={entityIdProp}
119
+ entity={entity}
120
+ dataLoading={dataLoading}/>;
121
+ }
122
+
123
+ export function EntityEditViewInner<M extends Record<string, any>>({
124
+ path,
125
+ entityId: entityIdProp,
126
+ selectedSubPath: selectedSubPathProp,
127
+ copy,
128
+ collection,
129
+ parentCollectionIds,
130
+ onValuesAreModified,
131
+ onUpdate,
132
+ onClose,
133
+ entity,
134
+ dataLoading,
135
+ }: EntityEditViewProps<M> & {
136
+ entity?: Entity<M>,
137
+ dataLoading: boolean
138
+ }) {
139
+
140
+ if (collection.customId && collection.formAutoSave) {
141
+ console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
142
+ }
143
+
144
+ const [saving, setSaving] = useState(false);
145
+ /**
146
+ * These are the values that are being saved. They are debounced.
147
+ * We use this only when autoSave is enabled.
148
+ */
149
+ const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
150
+ useDebouncedCallback(valuesToBeSaved, () => {
151
+ if (valuesToBeSaved)
152
+ saveEntity({
153
+ entityId: usedEntity?.id,
154
+ collection,
155
+ path,
156
+ values: valuesToBeSaved,
157
+ closeAfterSave: false
158
+ });
159
+ }, false, 2000);
160
+
161
+ // const largeLayout = useLargeLayout();
162
+ // const largeLayoutTabSelected = useRef(!largeLayout);
163
+ // const resolvedFormWidth: string = typeof formWidth === "number" ? `${formWidth}px` : formWidth ?? FORM_CONTAINER_WIDTH;
164
+
165
+ const inputCollection = collection;
166
+
167
+ const authController = useAuthController();
168
+ const dataSource = useDataSource(collection);
169
+ const sideDialogContext = useSideDialogContext();
170
+ const sideEntityController = useSideEntityController();
171
+ const snackbarController = useSnackbarController();
172
+ const customizationController = useCustomizationController();
173
+ const context = useFireCMSContext();
174
+
175
+ const closeAfterSaveRef = useRef(false);
176
+
177
+ const analyticsController = useAnalyticsController();
178
+
179
+ const initialResolvedCollection = useMemo(() => resolveCollection({
180
+ collection: inputCollection,
181
+ path,
182
+ values: entity?.values,
183
+ fields: customizationController.propertyConfigs
184
+ }), [entity?.values, path, customizationController.propertyConfigs]);
185
+
186
+ const initialStatus = copy ? "copy" : (entityIdProp ? "existing" : "new");
187
+ const [status, setStatus] = useState<EntityStatus>(initialStatus);
188
+ const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
189
+ (Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
190
+ const initialEntityId: string | undefined = useMemo((): string | undefined => {
191
+ if (status === "new" || status === "copy") {
192
+ if (mustSetCustomId) {
193
+ return undefined;
194
+ } else {
195
+ return dataSource.generateEntityId(path);
196
+ }
197
+ } else {
198
+ return entityIdProp;
199
+ }
200
+ }, [entityIdProp, status]);
201
+
202
+ const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
203
+
204
+ // const doOnValuesChanges = (values?: EntityValues<M>) => {
205
+ // const initialValues = formex.initialValues;
206
+ // setInternalValues(values);
207
+ // if (onValuesChanged)
208
+ // onValuesChanged(values);
209
+ // if (autoSave && values && !equal(values, initialValues)) {
210
+ // save(values);
211
+ // }
212
+ // };
213
+
214
+ const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
215
+ const [savingError, setSavingError] = React.useState<Error | undefined>();
216
+
217
+ const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
218
+
219
+ const defaultSelectedView = selectedSubPathProp ?? resolveDefaultSelectedView(
220
+ collection ? collection.defaultSelectedView : undefined,
221
+ {
222
+ status,
223
+ entityId
224
+ }
225
+ );
226
+
227
+ const selectedTabRef = useRef<string>(defaultSelectedView ?? MAIN_TAB_VALUE);
228
+ const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>>>(getDataSourceEntityValues(initialResolvedCollection, status, entity));
229
+
230
+ const mainViewVisible = selectedTabRef.current === MAIN_TAB_VALUE;
231
+
232
+ // const initialValuesRef = useRef<EntityValues<M>>(entity?.values ?? baseDataSourceValues as EntityValues<M>);
233
+ // const [internalValues, setInternalValues] = useState<EntityValues<M> | undefined>(entity?.values ?? baseDataSourceValuesRef.current as EntityValues<M>);
234
+
235
+ // const modifiedValuesRef = useRef<EntityValues<M> | undefined>(undefined);
236
+ // const modifiedValues = modifiedValuesRef.current;
237
+
238
+ const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
239
+ const subcollectionsCount = subcollections?.length ?? 0;
240
+ const customViews = collection.entityViews;
241
+ const customViewsCount = customViews?.length ?? 0;
242
+ const autoSave = collection.formAutoSave && !collection.customId;
243
+
244
+ const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
245
+
246
+ const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
247
+ const [readOnly, setReadOnly] = useState<boolean | undefined>(undefined);
248
+
249
+ useEffect(() => {
250
+ if (entity)
251
+ setUsedEntity(entity);
252
+ }, [entity]);
253
+
254
+ useEffect(() => {
255
+ if (status === "new") {
256
+ setReadOnly(false);
257
+ } else {
258
+ const editEnabled = usedEntity ? canEditEntity(collection, authController, path, usedEntity ?? null) : false;
259
+ if (usedEntity)
260
+ setReadOnly(!editEnabled);
261
+ }
262
+ }, [authController, usedEntity, status]);
263
+
264
+ const onPreSaveHookError = useCallback((e: Error) => {
265
+ setSaving(false);
266
+ snackbarController.open({
267
+ type: "error",
268
+ message: "Error before saving: " + e?.message
269
+ });
270
+ console.error(e);
271
+ }, [snackbarController]);
272
+
273
+ const onSaveSuccessHookError = useCallback((e: Error) => {
274
+ setSaving(false);
275
+ snackbarController.open({
276
+ type: "error",
277
+ message: "Error after saving (entity is saved): " + e?.message
278
+ });
279
+ console.error(e);
280
+ }, [snackbarController]);
281
+
282
+ const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
283
+
284
+ setSaving(false);
285
+ if (!autoSave)
286
+ snackbarController.open({
287
+ type: "success",
288
+ message: `${collection.singularName ?? collection.name}: Saved correctly`
289
+ });
290
+
291
+ setUsedEntity(updatedEntity);
292
+ setStatus("existing");
293
+
294
+ onValuesAreModified(false);
295
+
296
+ if (onUpdate)
297
+ onUpdate({ entity: updatedEntity });
298
+
299
+ if (closeAfterSave) {
300
+ console.log("Closing side dialog")
301
+ sideDialogContext.setBlocked(false);
302
+ sideDialogContext.close(true);
303
+ onClose?.();
304
+ } else if (status !== "existing") {
305
+ sideEntityController.replace({
306
+ path,
307
+ entityId: updatedEntity.id,
308
+ selectedSubPath: selectedTabRef.current,
309
+ updateUrl: true,
310
+ collection,
311
+ });
312
+ }
313
+
314
+ };
315
+
316
+ const onSaveFailure = useCallback((e: Error) => {
317
+
318
+ setSaving(false);
319
+ snackbarController.open({
320
+ type: "error",
321
+ message: "Error saving: " + e?.message
322
+ });
323
+
324
+ console.error("Error saving entity", path, entityId);
325
+ console.error(e);
326
+ }, [entityId, path, snackbarController]);
327
+
328
+ const saveEntity = ({
329
+ values,
330
+ previousValues,
331
+ closeAfterSave,
332
+ entityId,
333
+ collection,
334
+ path
335
+ }: {
336
+ collection: EntityCollection<M>,
337
+ path: string,
338
+ entityId: string | undefined,
339
+ values: M,
340
+ previousValues?: M,
341
+ closeAfterSave: boolean,
342
+ }) => {
343
+ setSaving(true);
344
+ return saveEntityWithCallbacks({
345
+ path,
346
+ entityId,
347
+ values,
348
+ previousValues,
349
+ collection,
350
+ status,
351
+ dataSource,
352
+ context,
353
+ onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity, closeAfterSave),
354
+ onSaveFailure,
355
+ onPreSaveHookError,
356
+ onSaveSuccessHookError
357
+ }).then();
358
+ };
359
+
360
+ const onSaveEntityRequest = async ({
361
+ collection,
362
+ path,
363
+ entityId,
364
+ values,
365
+ previousValues,
366
+ closeAfterSave,
367
+ autoSave
368
+ }: EntityFormSaveParams<M>): Promise<void> => {
369
+ if (!status)
370
+ return;
371
+
372
+ if (autoSave) {
373
+ setValuesToBeSaved(values);
374
+ } else {
375
+ return saveEntity({
376
+ collection,
377
+ path,
378
+ entityId,
379
+ values,
380
+ previousValues,
381
+ closeAfterSave
382
+ });
383
+ }
384
+ };
385
+
386
+ const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
387
+
388
+ if (mustSetCustomId && !entityId) {
389
+ console.error("Missing custom Id");
390
+ setEntityIdError(true);
391
+ formexController.setSubmitting(false);
392
+ return;
393
+ }
394
+
395
+ setSavingError(undefined);
396
+ setEntityIdError(false);
397
+
398
+ if (status === "existing") {
399
+ if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
400
+ } else if (status === "new" || status === "copy") {
401
+ if (inputCollection.customId) {
402
+ if (inputCollection.customId !== "optional" && !entityId) {
403
+ throw Error("Form misconfiguration when saving, entityId should be set");
404
+ }
405
+ }
406
+ } else {
407
+ throw Error("New FormType added, check EntityForm");
408
+ }
409
+
410
+ return save(values)
411
+ ?.then(_ => {
412
+ formexController.resetForm({
413
+ values,
414
+ submitCount: 0,
415
+ touched: {}
416
+ });
417
+ })
418
+ .finally(() => {
419
+ formexController.setSubmitting(false);
420
+ });
421
+
422
+ };
423
+
424
+ const formex: FormexController<M> = useCreateFormex<M>({
425
+ initialValues: baseDataSourceValuesRef.current as M,
426
+ onSubmit,
427
+ validation: (values) => {
428
+ return validationSchema?.validate(values, { abortEarly: false })
429
+ .then(() => {
430
+ return {};
431
+ })
432
+ .catch((e: any) => {
433
+ const errors: Record<string, string> = {};
434
+ e.inner.forEach((error: any) => {
435
+ errors[error.path] = error.message;
436
+ });
437
+ return yupToFormErrors(e);
438
+ });
439
+ }
440
+ });
441
+
442
+ const resolvedCollection = resolveCollection<M>({
443
+ collection: inputCollection,
444
+ path,
445
+ entityId,
446
+ values: formex.values,
447
+ previousValues: formex.initialValues,
448
+ fields: customizationController.propertyConfigs
449
+ });
450
+
451
+ const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
452
+
453
+ const save = (values: EntityValues<M>): Promise<void> => {
454
+ lastSavedValues.current = values;
455
+ return onSaveEntityRequest({
456
+ collection: resolvedCollection,
457
+ path,
458
+ entityId,
459
+ values,
460
+ previousValues: entity?.values,
461
+ closeAfterSave: closeAfterSaveRef.current,
462
+ autoSave: autoSave ?? false
463
+ }).then(_ => {
464
+ const eventName: CMSAnalyticsEvent = status === "new"
465
+ ? "new_entity_saved"
466
+ : (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
467
+ analyticsController.onAnalyticsEvent?.(eventName, { path });
468
+ }).catch(e => {
469
+ console.error(e);
470
+ setSavingError(e);
471
+ }).finally(() => {
472
+ closeAfterSaveRef.current = false;
473
+ });
474
+ };
475
+
476
+ const formContext: FormContext<M> = {
477
+ // @ts-ignore
478
+ setFieldValue: useCallback(formex.setFieldValue, []),
479
+ values: formex.values,
480
+ collection: resolvedCollection,
481
+ entityId,
482
+ path,
483
+ save,
484
+ formex
485
+ };
486
+
487
+ const resolvedEntityViews = customViews ? customViews
488
+ .map(e => resolveEntityView(e, customizationController.entityViews))
489
+ .filter(Boolean) as EntityCustomView[]
490
+ : [];
491
+
492
+ const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTabRef.current);
493
+ const shouldShowEntityActions = !autoSave && (selectedTabRef.current === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
494
+
495
+ const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
496
+ .map(
497
+ (customView, colIndex) => {
498
+ if (!customView)
499
+ return null;
500
+ if (selectedTabRef.current !== customView.key)
501
+ return null;
502
+ const Builder = customView.Builder;
503
+ if (!Builder) {
504
+ console.error("customView.Builder is not defined");
505
+ return null;
506
+ }
507
+ return <div
508
+ className={cls(defaultBorderMixin,
509
+ "relative flex-grow w-full h-full overflow-auto ")}
510
+ key={`custom_view_${customView.key}`}
511
+ role="tabpanel">
512
+ <ErrorBoundary>
513
+ {formContext && <Builder
514
+ collection={collection}
515
+ entity={usedEntity}
516
+ modifiedValues={formex.values ?? usedEntity?.values}
517
+ formContext={formContext}
518
+ />}
519
+ </ErrorBoundary>
520
+ </div>;
521
+ }
522
+ ).filter(Boolean);
523
+
524
+ const globalLoading = (dataLoading && !usedEntity) ||
525
+ ((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
526
+
527
+ const loading = globalLoading || saving;
528
+
529
+ const subCollectionsViews = subcollections && subcollections.map(
530
+ (subcollection, colIndex) => {
531
+ const subcollectionId = subcollection.id ?? subcollection.path;
532
+ const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
533
+ if (selectedTabRef.current !== subcollectionId)
534
+ return null;
535
+ return (
536
+ <div
537
+ className={"relative flex-grow h-full overflow-auto w-full"}
538
+ key={`subcol_${subcollectionId}`}
539
+ role="tabpanel">
540
+
541
+ {loading && <CircularProgressCenter/>}
542
+
543
+ {!globalLoading &&
544
+ (usedEntity && fullPath
545
+ ? <EntityCollectionView
546
+ fullPath={fullPath}
547
+ parentCollectionIds={[...parentCollectionIds, collection.id]}
548
+ isSubCollection={true}
549
+ {...subcollection}/>
550
+ : <div
551
+ className="flex items-center justify-center w-full h-full p-3">
552
+ <Typography variant={"label"}>
553
+ You need to save your entity before
554
+ adding additional collections
555
+ </Typography>
556
+ </div>)
557
+ }
558
+
559
+ </div>
560
+ );
561
+ }
562
+ ).filter(Boolean);
563
+
564
+ const onDiscard = useCallback(() => {
565
+ onValuesAreModified(false);
566
+ }, []);
567
+
568
+ const onSideTabClick = (value: string) => {
569
+ selectedTabRef.current = value;
570
+ sideEntityController.replace({
571
+ path,
572
+ entityId,
573
+ selectedSubPath: value === MAIN_TAB_VALUE ? undefined : value,
574
+ updateUrl: true,
575
+ collection,
576
+ });
577
+ };
578
+
579
+ // const onValuesChanged = useCallback((values?: EntityValues<M>) => {
580
+ // modifiedValuesRef.current = values;
581
+ // }, []);
582
+
583
+ // eslint-disable-next-line n/handle-callback-err
584
+ const onIdUpdateError = useCallback((error: any) => {
585
+ snackbarController.open({
586
+ type: "error",
587
+ message: "Error updating id, check the console"
588
+ });
589
+ }, []);
590
+
591
+ const onIdChange = useCallback((id: string) => {
592
+ setUsedEntity((prevEntity) => prevEntity
593
+ ? {
594
+ ...prevEntity,
595
+ id
596
+ }
597
+ : undefined);
598
+ }, []);
599
+
600
+ // useEffect(() => {
601
+ // baseDataSourceValuesRef.current = getDataSourceEntityValues(initialResolvedCollection, status, entity);
602
+ // const initialValues = formex.initialValues;
603
+ // if (!formex.isSubmitting && initialValues && status === "existing") {
604
+ // setUnderlyingChanges(
605
+ // Object.entries(resolvedCollection.properties)
606
+ // .map(([key, property]) => {
607
+ // if (isHidden(property)) {
608
+ // return {};
609
+ // }
610
+ // const initialValue = initialValues[key];
611
+ // const latestValue = baseDataSourceValuesRef.current[key];
612
+ // if (!equal(initialValue, latestValue)) {
613
+ // return { [key]: latestValue };
614
+ // }
615
+ // return {};
616
+ // })
617
+ // .reduce((a, b) => ({ ...a, ...b }), {}) as Partial<EntityValues<M>>
618
+ // );
619
+ // } else {
620
+ // setUnderlyingChanges({});
621
+ // }
622
+ // }, [entity, initialResolvedCollection, status]);
623
+
624
+ const pluginActions: React.ReactNode[] = [];
625
+
626
+ const plugins = customizationController.plugins;
627
+
628
+ if (plugins && inputCollection) {
629
+ const actionProps: PluginFormActionProps = {
630
+ entityId,
631
+ path,
632
+ status,
633
+ collection: inputCollection,
634
+ context,
635
+ currentEntityId: entityId,
636
+ formContext
637
+ };
638
+ pluginActions.push(...plugins.map((plugin, i) => (
639
+ plugin.form?.Actions
640
+ ? <plugin.form.Actions
641
+ key={`actions_${plugin.key}`} {...actionProps}/>
642
+ : null
643
+ )).filter(Boolean));
644
+ }
645
+
646
+ const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
647
+ const title = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
648
+
649
+ const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
650
+
651
+ const doOnIdUpdate = useCallback(async () => {
652
+ if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
653
+ setCustomIdLoading(true);
654
+ try {
655
+ const updatedId = await onIdUpdate({
656
+ collection: resolvedCollection,
657
+ path,
658
+ entityId,
659
+ values: formex.values,
660
+ context
661
+ });
662
+ setEntityId(updatedId);
663
+ } catch (e) {
664
+ onIdUpdateError && onIdUpdateError(e);
665
+ console.error(e);
666
+ }
667
+ setCustomIdLoading(false);
668
+ }
669
+ }, [entityId, formex.values, status]);
670
+
671
+ useEffect(() => {
672
+ doOnIdUpdate();
673
+ }, [doOnIdUpdate]);
674
+
675
+ const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
676
+
677
+ const uniqueFieldValidator: CustomFieldValidator = useCallback(({
678
+ name,
679
+ value,
680
+ property
681
+ }) => dataSource.checkUniqueField(path, name, value, entityId),
682
+ [dataSource, path, entityId]);
683
+
684
+ const validationSchema = useMemo(() => entityId
685
+ ? getYupEntitySchema(
686
+ entityId,
687
+ resolvedCollection.properties,
688
+ uniqueFieldValidator)
689
+ : undefined,
690
+ [entityId, resolvedCollection.properties, uniqueFieldValidator]);
691
+
692
+ const getActionsForEntity = useCallback(({
693
+ entity,
694
+ customEntityActions
695
+ }: {
696
+ entity?: Entity<M>,
697
+ customEntityActions?: EntityAction[]
698
+ }): EntityAction[] => {
699
+ const createEnabled = canCreateEntity(inputCollection, authController, path, null);
700
+ const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
701
+ const actions: EntityAction[] = [];
702
+ if (createEnabled)
703
+ actions.push(copyEntityAction);
704
+ if (deleteEnabled)
705
+ actions.push(deleteEntityAction);
706
+ if (customEntityActions)
707
+ actions.push(...customEntityActions);
708
+ return actions;
709
+ }, [authController, inputCollection, path]);
710
+
711
+ const modified = formex.dirty;
712
+ useEffect(() => {
713
+ if (!autoSave) {
714
+ onValuesAreModified(modified);
715
+ } else {
716
+ if (formex.values && !equal(formex.values, lastSavedValues.current)) {
717
+ save(formex.values);
718
+ }
719
+ }
720
+ }, [modified, formex.values]);
721
+
722
+ useEffect(() => {
723
+ if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
724
+ // we update the form fields from the Firestore data
725
+ // if they were not touched
726
+ Object.entries(underlyingChanges).forEach(([key, value]) => {
727
+ const formValue = formex.values[key];
728
+ if (!equal(value, formValue) && !formex.touched[key]) {
729
+ console.debug("Updated value from the datasource:", key, value);
730
+ formex.setFieldValue(key, value !== undefined ? value : null);
731
+ }
732
+ });
733
+ }
734
+ }, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
735
+
736
+ const formFields = (
737
+ <>
738
+ {(resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties))
739
+ .map((key) => {
740
+
741
+ const property = resolvedCollection.properties[key];
742
+ if (!property) {
743
+ console.warn(`Property ${key} not found in collection ${resolvedCollection.name}`);
744
+ return null;
745
+ }
746
+
747
+ const underlyingValueHasChanged: boolean =
748
+ !!underlyingChanges &&
749
+ Object.keys(underlyingChanges).includes(key) &&
750
+ !!formex.touched[key];
751
+
752
+ const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
753
+ const hidden = isHidden(property);
754
+ if (hidden) return null;
755
+ const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
756
+ propertyKey: key,
757
+ disabled,
758
+ property,
759
+ includeDescription: property.description || property.longDescription,
760
+ underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
761
+ context: formContext,
762
+ tableMode: false,
763
+ partOfArray: false,
764
+ partOfBlock: false,
765
+ autoFocus: false
766
+ };
767
+
768
+ return (
769
+ <div id={`form_field_${key}`}
770
+ key={`field_${resolvedCollection.name}_${key}`}>
771
+ <ErrorBoundary>
772
+ <Tooltip title={<PropertyIdCopyTooltipContent propertyId={key}/>}
773
+ delayDuration={800}
774
+ side={"left"}
775
+ align={"start"}
776
+ sideOffset={16}>
777
+ <PropertyFieldBinding {...cmsFormFieldProps}/>
778
+ </Tooltip>
779
+ </ErrorBoundary>
780
+ </div>
781
+ );
782
+ })
783
+ .filter(Boolean)}
784
+
785
+ </>
786
+ );
787
+
788
+ const disabled = formex.isSubmitting || (!modified && status === "existing");
789
+ const formRef = React.useRef<HTMLDivElement>(null);
790
+
791
+ const entityActions = getActionsForEntity({
792
+ entity,
793
+ customEntityActions: inputCollection.entityActions
794
+ });
795
+ const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
796
+
797
+ const dialogActions = <DialogActions position={"absolute"}>
798
+
799
+ {savingError &&
800
+ <div className="text-right">
801
+ <Typography color={"error"}>
802
+ {savingError.message}
803
+ </Typography>
804
+ </div>}
805
+
806
+ {entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
807
+ {formActions.map(action => (
808
+ <IconButton
809
+ key={action.name}
810
+ color="primary"
811
+ onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
812
+ event.stopPropagation();
813
+ if (entity)
814
+ action.onClick({
815
+ entity,
816
+ fullPath: resolvedCollection.path,
817
+ collection: resolvedCollection,
818
+ context,
819
+ sideEntityController
820
+ });
821
+ }}>
822
+ {action.icon}
823
+ </IconButton>
824
+ ))}
825
+ </div>}
826
+ {formex.isSubmitting && <CircularProgress size={"small"}/>}
827
+ <Button
828
+ variant="text"
829
+ disabled={disabled || formex.isSubmitting}
830
+ type="reset">
831
+ {status === "existing" ? "Discard" : "Clear"}
832
+ </Button>
833
+
834
+ <Button
835
+ variant="text"
836
+ color="primary"
837
+ type="submit"
838
+ disabled={disabled || formex.isSubmitting}
839
+ onClick={() => {
840
+ closeAfterSaveRef.current = false;
841
+ }}>
842
+ {status === "existing" && "Save"}
843
+ {status === "copy" && "Create copy"}
844
+ {status === "new" && "Create"}
845
+ </Button>
846
+
847
+ <Button
848
+ variant="filled"
849
+ color="primary"
850
+ type="submit"
851
+ disabled={disabled || formex.isSubmitting}
852
+ onClick={() => {
853
+ closeAfterSaveRef.current = true;
854
+ }}>
855
+ {status === "existing" && "Save and close"}
856
+ {status === "copy" && "Create copy and close"}
857
+ {status === "new" && "Create and close"}
858
+ </Button>
859
+
860
+ </DialogActions>;
861
+
862
+ function buildForm() {
863
+
864
+ let form = <div className="h-full overflow-auto">
865
+
866
+ {pluginActions.length > 0 && <div
867
+ className={cls("w-full flex justify-end items-center sticky top-0 right-0 left-0 z-10 bg-opacity-60 bg-slate-200 dark:bg-opacity-60 dark:bg-slate-800 backdrop-blur-md")}>
868
+ {pluginActions}
869
+ </div>}
870
+
871
+ <div className="pt-12 pb-16 pl-8 pr-8 md:pl-10 md:pr-10">
872
+ <div
873
+ className={`w-full py-2 flex flex-col items-start mt-${4 + (pluginActions ? 8 : 0)} lg:mt-${8 + (pluginActions ? 8 : 0)} mb-8`}>
874
+
875
+ <Typography
876
+ className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
877
+ variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
878
+ </Typography>
879
+ <Alert color={"base"} className={"w-full"} size={"small"}>
880
+ <code className={"text-xs select-all"}>{path}/{entityId}</code>
881
+ </Alert>
882
+ </div>
883
+
884
+ {!collection.hideIdFromForm &&
885
+ <CustomIdField customId={inputCollection.customId}
886
+ entityId={entityId}
887
+ status={status}
888
+ onChange={setEntityId}
889
+ error={entityIdError}
890
+ loading={customIdLoading}
891
+ entity={entity}/>}
892
+
893
+ {entityId && formContext && <>
894
+ <div className="mt-12 flex flex-col gap-8"
895
+ ref={formRef}>
896
+
897
+ {formFields}
898
+
899
+ <ErrorFocus containerRef={formRef}/>
900
+
901
+ </div>
902
+
903
+ <div className="h-14"/>
904
+
905
+ </>}
906
+
907
+ </div>
908
+ </div>;
909
+
910
+ if (plugins) {
911
+ plugins.forEach((plugin: FireCMSPlugin) => {
912
+ if (plugin.form?.provider) {
913
+ form = (
914
+ <plugin.form.provider.Component
915
+ status={status}
916
+ path={path}
917
+ collection={collection}
918
+ onDiscard={onDiscard}
919
+ entity={usedEntity}
920
+ context={context}
921
+ formContext={formContext}
922
+ {...plugin.form.provider.props}>
923
+ {form}
924
+ </plugin.form.provider.Component>
925
+ );
926
+ }
927
+ });
928
+ }
929
+ return <ErrorBoundary>{form}</ErrorBoundary>;
930
+ }
931
+
932
+ const entityView = (readOnly === undefined)
933
+ ? <></>
934
+ : (!readOnly
935
+ ? buildForm()
936
+ : (
937
+ <>
938
+ <Typography
939
+ className={"mt-16 mb-8 mx-8"}
940
+ variant={"h4"}>{collection.singularName ?? collection.name}
941
+ </Typography>
942
+ <EntityView
943
+ className={"px-12"}
944
+ entity={usedEntity as Entity<M>}
945
+ path={path}
946
+ collection={collection}/>
947
+
948
+ </>
949
+ ));
950
+
951
+ const subcollectionTabs = subcollections && subcollections.map(
952
+ (subcollection) =>
953
+ <Tab
954
+ className="text-sm min-w-[140px]"
955
+ value={subcollection.id}
956
+ key={`entity_detail_collection_tab_${subcollection.name}`}>
957
+ {subcollection.name}
958
+ </Tab>
959
+ );
960
+
961
+ const customViewTabs = resolvedEntityViews.map(
962
+ (view) =>
963
+
964
+ <Tab
965
+ className="text-sm min-w-[140px]"
966
+ value={view.key}
967
+ key={`entity_detail_collection_tab_${view.name}`}>
968
+ {view.name}
969
+ </Tab>
970
+ );
971
+
972
+ useEffect(() => {
973
+ if (entityId && onIdChange)
974
+ onIdChange(entityId);
975
+ }, [entityId, onIdChange]);
976
+
977
+ return (
978
+ <Formex value={formex}>
979
+
980
+ <div className="flex flex-col h-full w-full transition-width duration-250 ease-in-out">
981
+
982
+ <div
983
+ className={cls(defaultBorderMixin, "no-scrollbar border-b pl-2 pr-2 pt-1 flex items-end overflow-scroll bg-gray-50 dark:bg-gray-950")}>
984
+
985
+ <div
986
+ className="pb-1 self-center">
987
+ <IconButton
988
+ onClick={() => {
989
+ onClose?.();
990
+ return sideDialogContext.close(false);
991
+ }}
992
+ size="large">
993
+ <CloseIcon/>
994
+ </IconButton>
995
+ </div>
996
+
997
+ <div className={"flex-grow"}/>
998
+
999
+ {globalLoading && <div
1000
+ className="self-center">
1001
+ <CircularProgress size={"small"}/>
1002
+ </div>}
1003
+
1004
+ <Tabs
1005
+ value={selectedTabRef.current}
1006
+ onValueChange={(value) => {
1007
+ onSideTabClick(value);
1008
+ }}
1009
+ className="pl-4 pr-4 pt-0">
1010
+
1011
+ <Tab
1012
+ disabled={!hasAdditionalViews}
1013
+ value={MAIN_TAB_VALUE}
1014
+ className={`${
1015
+ !hasAdditionalViews ? "hidden" : ""
1016
+ } text-sm min-w-[140px]`}
1017
+ >{collection.singularName ?? collection.name}</Tab>
1018
+
1019
+ {customViewTabs}
1020
+
1021
+ {subcollectionTabs}
1022
+ </Tabs>
1023
+
1024
+ </div>
1025
+
1026
+ <form
1027
+ onSubmit={formex.handleSubmit}
1028
+ onReset={() => {
1029
+ formex.resetForm();
1030
+ return onDiscard && onDiscard();
1031
+ }}
1032
+ noValidate
1033
+ className={"flex-grow h-full flex overflow-auto flex-col w-full"}>
1034
+
1035
+ <div
1036
+ role="tabpanel"
1037
+ hidden={!mainViewVisible}
1038
+ id={`form_${path}`}
1039
+ className={" w-full"}>
1040
+
1041
+ {globalLoading
1042
+ ? <CircularProgressCenter/>
1043
+ : entityView}
1044
+
1045
+ </div>
1046
+
1047
+ {customViewsView}
1048
+
1049
+ {subCollectionsViews}
1050
+
1051
+ {shouldShowEntityActions && dialogActions}
1052
+
1053
+ </form>
1054
+
1055
+ </div>
1056
+ </Formex>
1057
+ );
1058
+ }
1059
+
1060
+ function getDataSourceEntityValues<M extends object>(initialResolvedCollection: ResolvedEntityCollection,
1061
+ status: "new" | "existing" | "copy",
1062
+ entity: Entity<M> | undefined): Partial<EntityValues<M>> {
1063
+
1064
+ const properties = initialResolvedCollection.properties;
1065
+ if ((status === "existing" || status === "copy") && entity) {
1066
+ return entity.values ?? getDefaultValuesFor(properties);
1067
+ } else if (status === "new") {
1068
+ return getDefaultValuesFor(properties);
1069
+ } else {
1070
+ console.error({
1071
+ status,
1072
+ entity
1073
+ });
1074
+ throw new Error("Form has not been initialised with the correct parameters");
1075
+ }
1076
+ }
1077
+
1078
+ export type EntityFormSaveParams<M extends Record<string, any>> = {
1079
+ collection: ResolvedEntityCollection<M>,
1080
+ path: string,
1081
+ entityId: string | undefined,
1082
+ values: EntityValues<M>,
1083
+ previousValues?: EntityValues<M>,
1084
+ closeAfterSave: boolean,
1085
+ autoSave: boolean
1086
+ };
1087
+
1088
+ export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
1089
+ let errors: Record<string, any> = {};
1090
+ if (yupError.inner) {
1091
+ if (yupError.inner.length === 0) {
1092
+ return setIn(errors, yupError.path!, yupError.message);
1093
+ }
1094
+ for (const err of yupError.inner) {
1095
+ if (!getIn(errors, err.path!)) {
1096
+ errors = setIn(errors, err.path!, err.message);
1097
+ }
1098
+ }
1099
+ }
1100
+ return errors;
1101
+ }