@firecms/core 3.0.0-canary.20 → 3.0.0-canary.200

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