@firecms/core 3.0.0-canary.28 → 3.0.0-canary.281

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