@firecms/core 3.0.0-canary.19 → 3.0.0-canary.191

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