@firecms/core 3.0.0-canary.1 → 3.0.0-canary.100

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