@firecms/core 3.0.0-canary.29 → 3.0.0-canary.290

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