@firecms/core 3.0.0-canary.18 → 3.0.0-canary.180
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.
- package/README.md +3 -3
- package/dist/app/AppBar.d.ts +12 -0
- package/dist/app/Drawer.d.ts +16 -0
- package/dist/app/Scaffold.d.ts +30 -0
- package/dist/app/index.d.ts +4 -0
- package/dist/app/useApp.d.ts +16 -0
- package/dist/components/ArrayContainer.d.ts +23 -6
- package/dist/components/CircularProgressCenter.d.ts +1 -1
- package/dist/components/ClearFilterSortButton.d.ts +5 -0
- package/dist/components/{DeleteConfirmationDialog.d.ts → ConfirmationDialog.d.ts} +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +13 -13
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +22 -6
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +1 -2
- package/dist/components/EntityCollectionTable/fields/TableReferenceField.d.ts +3 -1
- package/dist/components/EntityCollectionTable/index.d.ts +1 -1
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
- package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +2 -2
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +7 -4
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +16 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
- package/dist/components/EntityCollectionView/utils.d.ts +3 -0
- package/dist/components/EntityPreview.d.ts +5 -4
- package/dist/components/ErrorView.d.ts +1 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +1 -1
- package/dist/components/HomePage/NavigationCardBinding.d.ts +1 -1
- package/dist/components/PropertyConfigBadge.d.ts +2 -1
- package/dist/components/PropertyIdCopyTooltip.d.ts +8 -0
- package/dist/components/ReferenceWidget.d.ts +3 -1
- package/dist/components/SelectableTable/SelectableTable.d.ts +13 -3
- package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +2 -1
- package/dist/components/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +28 -12
- package/dist/components/VirtualTable/types.d.ts +3 -3
- package/dist/components/{EntityCollectionTable/internal → common}/default_entity_actions.d.ts +1 -3
- package/dist/components/common/index.d.ts +2 -1
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +4 -6
- package/dist/components/common/useColumnsIds.d.ts +3 -1
- package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +13 -2
- package/dist/components/common/useDebouncedCallback.d.ts +1 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +4 -2
- package/dist/contexts/BreacrumbsContext.d.ts +8 -0
- package/dist/{components/FireCMSAppBar.d.ts → core/DefaultAppBar.d.ts} +6 -9
- package/dist/core/DefaultDrawer.d.ts +19 -0
- package/dist/core/DrawerNavigationItem.d.ts +10 -0
- package/dist/core/EntityEditView.d.ts +41 -9
- package/dist/core/FireCMS.d.ts +2 -2
- package/dist/core/FireCMSRouter.d.ts +4 -0
- package/dist/core/NavigationRoutes.d.ts +2 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/ErrorFocus.d.ts +1 -1
- package/dist/form/components/FieldHelperText.d.ts +3 -3
- package/dist/form/components/LabelWithIcon.d.ts +1 -1
- package/dist/form/components/LabelWithIconAndTooltip.d.ts +15 -0
- package/dist/form/components/StorageItemPreview.d.ts +4 -4
- package/dist/form/components/index.d.ts +1 -1
- package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +11 -0
- package/dist/form/field_bindings/{MultiSelectBinding.d.ts → MultiSelectFieldBinding.d.ts} +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReferenceFieldBinding.d.ts +2 -2
- package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -5
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -2
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/index.d.ts +3 -5
- package/dist/form/useClearRestoreValue.d.ts +2 -2
- package/dist/hooks/data/delete.d.ts +4 -4
- package/dist/hooks/data/save.d.ts +4 -5
- package/dist/hooks/data/useCollectionFetch.d.ts +1 -1
- package/dist/hooks/data/useEntityFetch.d.ts +4 -3
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useAuthController.d.ts +1 -1
- package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
- package/dist/hooks/useBuildNavigationController.d.ts +3 -5
- package/dist/hooks/useFireCMSContext.d.ts +1 -1
- package/dist/hooks/useProjectLog.d.ts +8 -2
- package/dist/hooks/useResolvedNavigationFrom.d.ts +3 -3
- package/dist/hooks/useValidateAuthenticator.d.ts +4 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +21537 -14243
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +23973 -588
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +1 -16
- package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
- package/dist/preview/PropertyPreviewProps.d.ts +6 -1
- package/dist/preview/components/EnumValuesChip.d.ts +1 -1
- package/dist/preview/components/ReferencePreview.d.ts +3 -2
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/util.d.ts +3 -3
- package/dist/routes/CustomCMSRoute.d.ts +4 -0
- package/dist/routes/FireCMSRoute.d.ts +1 -0
- package/dist/routes/HomePageRoute.d.ts +3 -0
- package/dist/types/auth.d.ts +8 -10
- package/dist/types/collections.d.ts +71 -22
- package/dist/types/datasource.d.ts +52 -36
- package/dist/types/dialogs_controller.d.ts +7 -3
- package/dist/types/entities.d.ts +7 -2
- package/dist/types/entity_actions.d.ts +28 -4
- package/dist/types/entity_callbacks.d.ts +16 -16
- package/dist/types/entity_overrides.d.ts +2 -2
- package/dist/types/export_import.d.ts +4 -4
- package/dist/types/fields.d.ts +66 -42
- package/dist/types/firecms.d.ts +6 -2
- package/dist/types/firecms_context.d.ts +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/navigation.d.ts +31 -14
- package/dist/types/permissions.d.ts +4 -4
- package/dist/types/plugins.d.ts +23 -12
- package/dist/types/properties.d.ts +51 -23
- package/dist/types/property_config.d.ts +0 -2
- package/dist/types/side_dialogs_controller.d.ts +10 -0
- package/dist/types/side_entity_controller.d.ts +1 -1
- package/dist/types/storage.d.ts +86 -3
- package/dist/types/user.d.ts +1 -0
- package/dist/util/builders.d.ts +3 -3
- package/dist/util/entities.d.ts +3 -3
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +23 -0
- package/dist/util/icon_list.d.ts +5 -1
- package/dist/util/icon_synonyms.d.ts +1 -98
- package/dist/util/icons.d.ts +6 -3
- package/dist/util/index.d.ts +2 -0
- package/dist/util/navigation_from_path.d.ts +5 -1
- package/dist/util/navigation_utils.d.ts +14 -3
- package/dist/util/objects.d.ts +2 -1
- package/dist/util/permissions.d.ts +4 -4
- package/dist/util/plurals.d.ts +0 -2
- package/dist/util/property_utils.d.ts +3 -3
- package/dist/util/resolutions.d.ts +30 -16
- package/dist/util/storage.d.ts +23 -2
- package/dist/util/useStorageUploadController.d.ts +3 -3
- package/package.json +61 -48
- package/src/app/AppBar.tsx +18 -0
- package/src/app/Drawer.tsx +24 -0
- package/src/app/Scaffold.tsx +243 -0
- package/src/app/index.ts +4 -0
- package/src/app/useApp.tsx +32 -0
- package/src/components/ArrayContainer.tsx +126 -40
- package/src/components/CircularProgressCenter.tsx +2 -2
- package/src/components/ClearFilterSortButton.tsx +41 -0
- package/src/components/{DeleteConfirmationDialog.tsx → ConfirmationDialog.tsx} +3 -3
- package/src/components/DeleteEntityDialog.tsx +10 -19
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +54 -37
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +38 -31
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +30 -9
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +47 -39
- package/src/components/EntityCollectionTable/column_utils.tsx +3 -3
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +30 -16
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +19 -17
- package/src/components/EntityCollectionTable/index.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +31 -36
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +36 -34
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +20 -8
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +84 -77
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +206 -112
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
- package/src/components/EntityCollectionView/useSelectionController.tsx +20 -7
- package/src/components/EntityCollectionView/utils.ts +19 -0
- package/src/components/EntityPreview.tsx +62 -52
- package/src/components/EntityView.tsx +10 -10
- package/src/components/ErrorView.tsx +4 -4
- package/src/components/HomePage/DefaultHomePage.tsx +26 -19
- package/src/components/HomePage/FavouritesView.tsx +6 -11
- package/src/components/HomePage/NavigationCard.tsx +3 -3
- package/src/components/HomePage/NavigationCardBinding.tsx +5 -10
- package/src/components/HomePage/NavigationGroup.tsx +2 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +5 -6
- package/src/components/NotFoundPage.tsx +2 -2
- package/src/components/PropertyConfigBadge.tsx +9 -3
- package/src/components/PropertyIdCopyTooltip.tsx +47 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -13
- package/src/components/ReferenceWidget.tsx +21 -11
- package/src/components/SearchIconsView.tsx +10 -7
- package/src/components/SelectableTable/SelectableTable.tsx +27 -12
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +2 -3
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +25 -8
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +36 -12
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +92 -23
- package/src/components/UnsavedChangesDialog.tsx +42 -0
- package/src/components/VirtualTable/VirtualTable.tsx +96 -48
- package/src/components/VirtualTable/VirtualTableCell.tsx +1 -9
- package/src/components/VirtualTable/VirtualTableHeader.tsx +10 -10
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +2 -2
- package/src/components/VirtualTable/VirtualTableProps.tsx +33 -14
- package/src/components/VirtualTable/VirtualTableRow.tsx +5 -6
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +5 -5
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +3 -2
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +2 -1
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +16 -28
- package/src/components/VirtualTable/types.tsx +2 -3
- package/src/components/{EntityCollectionTable/internal → common}/default_entity_actions.tsx +33 -40
- package/src/components/common/index.ts +2 -1
- package/src/components/{VirtualTable/common.tsx → common/table_height.tsx} +5 -2
- package/src/components/common/types.tsx +4 -6
- package/src/components/common/useColumnsIds.tsx +24 -3
- package/src/components/common/useDataSourceTableController.tsx +420 -0
- package/src/components/common/useDebouncedCallback.tsx +20 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/common/useTableSearchHelper.ts +53 -12
- package/src/components/index.tsx +4 -2
- package/src/contexts/BreacrumbsContext.tsx +38 -0
- package/src/contexts/DialogsProvider.tsx +5 -4
- package/src/contexts/SnackbarProvider.tsx +2 -0
- package/src/core/DefaultAppBar.tsx +201 -0
- package/src/core/DefaultDrawer.tsx +185 -0
- package/src/core/DrawerNavigationItem.tsx +66 -0
- package/src/core/EntityEditView.tsx +1001 -213
- package/src/core/EntitySidePanel.tsx +80 -17
- package/src/core/FireCMS.tsx +56 -49
- package/src/core/FireCMSRouter.tsx +17 -0
- package/src/core/NavigationRoutes.tsx +20 -34
- package/src/core/SideDialogs.tsx +12 -6
- package/src/core/field_configs.tsx +11 -12
- package/src/core/index.tsx +6 -5
- package/src/form/PropertyFieldBinding.tsx +85 -44
- package/src/form/components/CustomIdField.tsx +6 -2
- package/src/form/components/FieldHelperText.tsx +4 -4
- package/src/form/components/LabelWithIcon.tsx +30 -19
- package/src/form/components/LabelWithIconAndTooltip.tsx +28 -0
- package/src/form/components/StorageItemPreview.tsx +22 -12
- package/src/form/components/StorageUploadProgress.tsx +3 -4
- package/src/form/components/index.tsx +1 -1
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +31 -17
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +47 -35
- package/src/form/field_bindings/BlockFieldBinding.tsx +56 -34
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +18 -14
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +60 -51
- package/src/form/field_bindings/MapFieldBinding.tsx +72 -53
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +150 -0
- package/src/form/field_bindings/{MultiSelectBinding.tsx → MultiSelectFieldBinding.tsx} +25 -20
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +11 -16
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +42 -31
- package/src/form/field_bindings/RepeatFieldBinding.tsx +56 -35
- package/src/form/field_bindings/SelectFieldBinding.tsx +22 -14
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +37 -47
- package/src/form/field_bindings/SwitchFieldBinding.tsx +29 -24
- package/src/form/field_bindings/TextFieldBinding.tsx +28 -24
- package/src/form/index.tsx +4 -9
- package/src/form/useClearRestoreValue.tsx +2 -2
- package/src/form/validation.ts +1 -17
- package/src/hooks/data/delete.ts +6 -5
- package/src/hooks/data/save.ts +13 -9
- package/src/hooks/data/useCollectionFetch.tsx +3 -3
- package/src/hooks/data/useDataSource.tsx +11 -3
- package/src/hooks/data/useEntityFetch.tsx +10 -6
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useAuthController.tsx +1 -1
- package/src/hooks/useBreadcrumbsController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +8 -10
- package/src/hooks/useBuildModeController.tsx +11 -5
- package/src/hooks/useBuildNavigationController.tsx +150 -50
- package/src/hooks/useFireCMSContext.tsx +5 -5
- package/src/hooks/useLargeLayout.tsx +0 -35
- package/src/hooks/useProjectLog.tsx +32 -10
- package/src/hooks/useResolvedNavigationFrom.tsx +10 -12
- package/src/hooks/useValidateAuthenticator.tsx +17 -37
- package/src/index.ts +1 -0
- package/src/internal/useBuildDataSource.ts +72 -82
- package/src/internal/useBuildSideDialogsController.tsx +4 -2
- package/src/internal/useBuildSideEntityController.tsx +230 -70
- package/src/internal/useUnsavedChangesDialog.tsx +127 -91
- package/src/preview/PropertyPreview.tsx +13 -20
- package/src/preview/PropertyPreviewProps.tsx +7 -1
- package/src/preview/components/BooleanPreview.tsx +2 -2
- package/src/preview/components/EmptyValue.tsx +1 -1
- package/src/preview/components/EnumValuesChip.tsx +2 -2
- package/src/preview/components/ImagePreview.tsx +25 -36
- package/src/preview/components/ReferencePreview.tsx +22 -33
- package/src/preview/components/StorageThumbnail.tsx +5 -1
- package/src/preview/components/UrlComponentPreview.tsx +44 -11
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +2 -3
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -3
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +2 -3
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +1 -2
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +4 -5
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +4 -5
- package/src/preview/property_previews/MapPropertyPreview.tsx +9 -9
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +12 -12
- package/src/preview/property_previews/StringPropertyPreview.tsx +3 -3
- package/src/preview/util.ts +10 -10
- package/src/routes/CustomCMSRoute.tsx +21 -0
- package/src/routes/FireCMSRoute.tsx +230 -0
- package/src/routes/HomePageRoute.tsx +17 -0
- package/src/types/auth.tsx +9 -13
- package/src/types/collections.ts +87 -27
- package/src/types/customization_controller.tsx +0 -1
- package/src/types/datasource.ts +61 -43
- package/src/types/dialogs_controller.tsx +7 -3
- package/src/types/entities.ts +12 -2
- package/src/types/entity_actions.tsx +32 -7
- package/src/types/entity_callbacks.ts +18 -18
- package/src/types/entity_overrides.tsx +2 -2
- package/src/types/export_import.ts +4 -4
- package/src/types/fields.tsx +72 -46
- package/src/types/firecms.tsx +7 -2
- package/src/types/firecms_context.tsx +1 -1
- package/src/types/index.ts +0 -1
- package/src/types/navigation.ts +37 -18
- package/src/types/permissions.ts +5 -5
- package/src/types/plugins.tsx +25 -14
- package/src/types/properties.ts +59 -24
- package/src/types/property_config.tsx +0 -2
- package/src/types/side_dialogs_controller.tsx +13 -0
- package/src/types/side_entity_controller.tsx +1 -1
- package/src/types/storage.ts +94 -3
- package/src/types/user.ts +2 -0
- package/src/util/builders.ts +10 -8
- package/src/util/entities.ts +9 -6
- package/src/util/entity_actions.ts +28 -0
- package/src/util/entity_cache.ts +196 -0
- package/src/util/enums.ts +1 -1
- package/src/util/icon_list.ts +16 -10
- package/src/util/icon_synonyms.ts +3 -100
- package/src/util/icons.tsx +17 -7
- package/src/util/index.ts +2 -0
- package/src/util/join_collections.ts +3 -0
- package/src/util/make_properties_editable.ts +13 -5
- package/src/util/navigation_from_path.ts +10 -2
- package/src/util/navigation_utils.ts +59 -7
- package/src/util/objects.ts +45 -28
- package/src/util/parent_references_from_path.ts +3 -3
- package/src/util/permissions.ts +9 -8
- package/src/util/plurals.ts +0 -2
- package/src/util/property_utils.tsx +11 -4
- package/src/util/references.ts +11 -2
- package/src/util/resolutions.ts +79 -46
- package/src/util/storage.ts +79 -21
- package/src/util/strings.ts +2 -2
- package/src/util/useStorageUploadController.tsx +34 -8
- package/dist/components/PropertyIdCopyTooltipContent.d.ts +0 -3
- package/dist/components/VirtualTable/common.d.ts +0 -2
- package/dist/core/Drawer.d.ts +0 -23
- package/dist/core/Scaffold.d.ts +0 -55
- package/dist/core/SideEntityView.d.ts +0 -7
- package/dist/form/EntityForm.d.ts +0 -77
- package/dist/form/components/FormikArrayContainer.d.ts +0 -18
- package/dist/form/field_bindings/MarkdownFieldBinding.d.ts +0 -9
- package/dist/internal/useBuildCustomizationController.d.ts +0 -2
- package/dist/internal/useLocaleConfig.d.ts +0 -1
- package/dist/types/appcheck.d.ts +0 -26
- package/src/components/FireCMSAppBar.tsx +0 -165
- package/src/components/PropertyIdCopyTooltipContent.tsx +0 -28
- package/src/components/common/useDataSourceEntityCollectionTableController.tsx +0 -225
- package/src/core/Drawer.tsx +0 -191
- package/src/core/Scaffold.tsx +0 -281
- package/src/core/SideEntityView.tsx +0 -38
- package/src/form/EntityForm.tsx +0 -728
- package/src/form/components/FormikArrayContainer.tsx +0 -44
- package/src/form/field_bindings/MarkdownFieldBinding.tsx +0 -695
- package/src/internal/useBuildCustomizationController.tsx +0 -5
- package/src/internal/useLocaleConfig.tsx +0 -18
- package/src/types/appcheck.ts +0 -29
- /package/src/util/{common.tsx → common.ts} +0 -0
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
CMSAnalyticsEvent,
|
|
3
4
|
Entity,
|
|
5
|
+
EntityAction,
|
|
4
6
|
EntityCollection,
|
|
5
7
|
EntityCustomView,
|
|
6
8
|
EntityStatus,
|
|
7
9
|
EntityValues,
|
|
10
|
+
FireCMSContext,
|
|
8
11
|
FireCMSPlugin,
|
|
9
12
|
FormContext,
|
|
13
|
+
PluginFormActionProps,
|
|
14
|
+
PropertyFieldBindingProps,
|
|
15
|
+
ResolvedEntityCollection,
|
|
16
|
+
SideEntityController,
|
|
10
17
|
User
|
|
11
18
|
} from "../types";
|
|
12
|
-
import
|
|
19
|
+
import equal from "react-fast-compare"
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
CircularProgressCenter,
|
|
23
|
+
copyEntityAction,
|
|
24
|
+
deleteEntityAction,
|
|
25
|
+
EntityCollectionView,
|
|
26
|
+
EntityView,
|
|
27
|
+
ErrorBoundary,
|
|
28
|
+
getFormFieldKeys
|
|
29
|
+
} from "../components";
|
|
13
30
|
import {
|
|
31
|
+
canCreateEntity,
|
|
32
|
+
canDeleteEntity,
|
|
14
33
|
canEditEntity,
|
|
34
|
+
getDefaultValuesFor,
|
|
35
|
+
getEntityTitlePropertyKey,
|
|
36
|
+
getValueInPath,
|
|
37
|
+
isHidden,
|
|
38
|
+
isReadOnly,
|
|
39
|
+
mergeEntityActions,
|
|
15
40
|
removeInitialAndTrailingSlashes,
|
|
41
|
+
resolveCollection,
|
|
16
42
|
resolveDefaultSelectedView,
|
|
17
43
|
resolveEntityView,
|
|
18
44
|
useDebouncedCallback
|
|
@@ -25,49 +51,177 @@ import {
|
|
|
25
51
|
useDataSource,
|
|
26
52
|
useEntityFetch,
|
|
27
53
|
useFireCMSContext,
|
|
54
|
+
useLargeLayout,
|
|
28
55
|
useSideEntityController,
|
|
29
56
|
useSnackbarController
|
|
30
57
|
} from "../hooks";
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
import {
|
|
59
|
+
Alert,
|
|
60
|
+
Button,
|
|
61
|
+
CheckIcon,
|
|
62
|
+
Chip,
|
|
63
|
+
CircularProgress,
|
|
64
|
+
cls,
|
|
65
|
+
defaultBorderMixin,
|
|
66
|
+
DialogActions,
|
|
67
|
+
EditIcon,
|
|
68
|
+
IconButton,
|
|
69
|
+
LoadingButton,
|
|
70
|
+
NotesIcon,
|
|
71
|
+
paperMixin,
|
|
72
|
+
Tab,
|
|
73
|
+
Tabs,
|
|
74
|
+
Tooltip,
|
|
75
|
+
Typography
|
|
76
|
+
} from "@firecms/ui";
|
|
77
|
+
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
78
|
+
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
79
|
+
import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
|
|
80
|
+
import { LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
81
|
+
import { ValidationError } from "yup";
|
|
82
|
+
import { getEntityFromCache, removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
|
|
83
|
+
import { CustomIdField } from "../form/components/CustomIdField";
|
|
84
|
+
import { ErrorFocus } from "../form/components/ErrorFocus";
|
|
36
85
|
|
|
37
86
|
const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
|
|
38
87
|
|
|
88
|
+
export type OnUpdateParams = {
|
|
89
|
+
entity: Entity<any>,
|
|
90
|
+
status: EntityStatus,
|
|
91
|
+
path: string
|
|
92
|
+
entityId?: string;
|
|
93
|
+
selectedTab?: string;
|
|
94
|
+
collection: EntityCollection<any>
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type OnTabChangeParams<M extends Record<string, any>> = {
|
|
98
|
+
path: string;
|
|
99
|
+
entityId?: string;
|
|
100
|
+
selectedTab?: string;
|
|
101
|
+
collection: EntityCollection<M>;
|
|
102
|
+
};
|
|
103
|
+
|
|
39
104
|
export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
40
105
|
path: string;
|
|
41
106
|
collection: EntityCollection<M>;
|
|
42
107
|
entityId?: string;
|
|
108
|
+
databaseId?: string;
|
|
43
109
|
copy?: boolean;
|
|
44
|
-
|
|
110
|
+
selectedTab?: string;
|
|
45
111
|
parentCollectionIds: string[];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onUpdate?: (params: { entity: Entity<any> }) => void;
|
|
112
|
+
onValuesModified?: (modified: boolean) => void;
|
|
113
|
+
onSaved?: (params: OnUpdateParams) => void;
|
|
49
114
|
onClose?: () => void;
|
|
115
|
+
onTabChange?: (props: OnTabChangeParams<M>) => void;
|
|
116
|
+
layout?: "side_panel" | "full_screen";
|
|
117
|
+
barActions?: React.ReactNode;
|
|
50
118
|
}
|
|
51
119
|
|
|
52
120
|
/**
|
|
53
121
|
* This is the default view that is used as the content of a side panel when
|
|
54
122
|
* an entity is opened.
|
|
55
123
|
* You probably don't want to use this view directly since it is bound to the
|
|
56
|
-
* side panel.
|
|
124
|
+
* side panel.
|
|
57
125
|
*/
|
|
58
|
-
export function EntityEditView<M extends Record<string, any>,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
126
|
+
export function EntityEditView<M extends Record<string, any>, USER extends User>({
|
|
127
|
+
entityId,
|
|
128
|
+
...props
|
|
129
|
+
}: EntityEditViewProps<M>) {
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
entity,
|
|
133
|
+
dataLoading,
|
|
134
|
+
// eslint-disable-next-line no-unused-vars
|
|
135
|
+
dataLoadingError
|
|
136
|
+
} = useEntityFetch<M, USER>({
|
|
137
|
+
path: props.path,
|
|
138
|
+
entityId: entityId,
|
|
139
|
+
collection: props.collection,
|
|
140
|
+
databaseId: props.databaseId,
|
|
141
|
+
useCache: false
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const cachedValues = entityId ? getEntityFromCache(props.path + "/" + entityId) : getEntityFromCache(props.path + "#new");
|
|
145
|
+
|
|
146
|
+
if (dataLoading && !cachedValues) {
|
|
147
|
+
return <CircularProgressCenter/>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (entityId && !entity && !cachedValues) {
|
|
151
|
+
console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
|
|
152
|
+
}
|
|
70
153
|
|
|
154
|
+
return <EntityEditViewInner<M> {...props}
|
|
155
|
+
entityId={entityId}
|
|
156
|
+
entity={entity}
|
|
157
|
+
cachedDirtyValues={cachedValues as Partial<M>}
|
|
158
|
+
dataLoading={dataLoading}
|
|
159
|
+
/>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function FormLayout({
|
|
163
|
+
id,
|
|
164
|
+
formex,
|
|
165
|
+
children,
|
|
166
|
+
className
|
|
167
|
+
}: {
|
|
168
|
+
id?: string,
|
|
169
|
+
formex: FormexController<any>,
|
|
170
|
+
children: React.ReactNode,
|
|
171
|
+
className?: string
|
|
172
|
+
}) {
|
|
173
|
+
|
|
174
|
+
return <div
|
|
175
|
+
role="tabpanel"
|
|
176
|
+
id={id}
|
|
177
|
+
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", className)}>
|
|
178
|
+
|
|
179
|
+
<div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
|
|
180
|
+
|
|
181
|
+
{formex.dirty
|
|
182
|
+
? <Tooltip title={"Unsaved changes"}
|
|
183
|
+
className={"self-end sticky top-4 z-10"}>
|
|
184
|
+
<Chip size={"small"} colorScheme={"orangeDarker"}>
|
|
185
|
+
<EditIcon size={"smallest"}/>
|
|
186
|
+
</Chip>
|
|
187
|
+
</Tooltip>
|
|
188
|
+
: <Tooltip title={"In sync with the database"}
|
|
189
|
+
className={"self-end sticky top-4 z-10"}>
|
|
190
|
+
<Chip size={"small"}>
|
|
191
|
+
<CheckIcon size={"smallest"}/>
|
|
192
|
+
</Chip>
|
|
193
|
+
</Tooltip>}
|
|
194
|
+
|
|
195
|
+
{children}
|
|
196
|
+
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
</div>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
203
|
+
path,
|
|
204
|
+
entityId: entityIdProp,
|
|
205
|
+
selectedTab: selectedTabProp,
|
|
206
|
+
copy,
|
|
207
|
+
collection,
|
|
208
|
+
parentCollectionIds,
|
|
209
|
+
onValuesModified,
|
|
210
|
+
onSaved,
|
|
211
|
+
onClose,
|
|
212
|
+
onTabChange,
|
|
213
|
+
entity,
|
|
214
|
+
cachedDirtyValues,
|
|
215
|
+
dataLoading,
|
|
216
|
+
layout = "side_panel",
|
|
217
|
+
barActions
|
|
218
|
+
}: EntityEditViewProps<M> & {
|
|
219
|
+
entity?: Entity<M>,
|
|
220
|
+
cachedDirtyValues?: Partial<M>, // dirty cached entity in memory
|
|
221
|
+
dataLoading: boolean,
|
|
222
|
+
}) {
|
|
223
|
+
|
|
224
|
+
const largeLayout = useLargeLayout();
|
|
71
225
|
if (collection.customId && collection.formAutoSave) {
|
|
72
226
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
73
227
|
}
|
|
@@ -89,76 +243,96 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
89
243
|
});
|
|
90
244
|
}, false, 2000);
|
|
91
245
|
|
|
92
|
-
|
|
93
|
-
// const largeLayoutTabSelected = useRef(!largeLayout);
|
|
94
|
-
|
|
95
|
-
const resolvedFormWidth: string = typeof formWidth === "number" ? `${formWidth}px` : formWidth ?? FORM_CONTAINER_WIDTH;
|
|
246
|
+
const inputCollection = collection;
|
|
96
247
|
|
|
248
|
+
const authController = useAuthController();
|
|
97
249
|
const dataSource = useDataSource(collection);
|
|
98
|
-
const sideDialogContext = useSideDialogContext();
|
|
99
250
|
const sideEntityController = useSideEntityController();
|
|
100
251
|
const snackbarController = useSnackbarController();
|
|
101
252
|
const customizationController = useCustomizationController();
|
|
102
253
|
const context = useFireCMSContext();
|
|
103
|
-
const authController = useAuthController<UserType>();
|
|
104
254
|
|
|
105
|
-
const
|
|
255
|
+
const closeAfterSaveRef = useRef(false);
|
|
106
256
|
|
|
107
|
-
const
|
|
257
|
+
const analyticsController = useAnalyticsController();
|
|
108
258
|
|
|
109
|
-
const
|
|
110
|
-
|
|
259
|
+
const initialResolvedCollection = useMemo(() => resolveCollection({
|
|
260
|
+
collection: inputCollection,
|
|
261
|
+
path,
|
|
262
|
+
values: entity?.values,
|
|
263
|
+
propertyConfigs: customizationController.propertyConfigs
|
|
264
|
+
}), [entity?.values, path, customizationController.propertyConfigs]);
|
|
265
|
+
|
|
266
|
+
const initialStatus = copy ? "copy" : (entityIdProp ? "existing" : "new");
|
|
267
|
+
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
268
|
+
|
|
269
|
+
const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
|
|
270
|
+
(Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
|
|
271
|
+
|
|
272
|
+
const initialEntityId: string | undefined = useMemo((): string | undefined => {
|
|
273
|
+
if (status === "new" || status === "copy") {
|
|
274
|
+
if (mustSetCustomId) {
|
|
275
|
+
return undefined;
|
|
276
|
+
} else {
|
|
277
|
+
return dataSource.generateEntityId(path, collection);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
return entityIdProp;
|
|
281
|
+
}
|
|
282
|
+
}, [entityIdProp, status]);
|
|
111
283
|
|
|
112
|
-
const
|
|
113
|
-
const subcollectionsCount = subcollections?.length ?? 0;
|
|
114
|
-
const customViews = collection.entityViews;
|
|
115
|
-
const customViewsCount = customViews?.length ?? 0;
|
|
116
|
-
const autoSave = collection.formAutoSave && !collection.customId;
|
|
284
|
+
const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
|
|
117
285
|
|
|
118
|
-
const
|
|
286
|
+
const [entityIdError, setEntityIdError] = React.useState<boolean>(false);
|
|
287
|
+
const [savingError, setSavingError] = React.useState<Error | undefined>();
|
|
119
288
|
|
|
120
|
-
const
|
|
289
|
+
const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
|
|
290
|
+
|
|
291
|
+
const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
|
|
121
292
|
collection ? collection.defaultSelectedView : undefined,
|
|
122
293
|
{
|
|
123
294
|
status,
|
|
124
295
|
entityId
|
|
125
296
|
}
|
|
126
|
-
);
|
|
297
|
+
), []);
|
|
127
298
|
|
|
128
|
-
const
|
|
299
|
+
const [selectedTab, setSelectedTab] = useState<string>(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
|
|
129
300
|
|
|
130
|
-
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if ((selectedTabProp ?? MAIN_TAB_VALUE) !== selectedTab) {
|
|
303
|
+
setSelectedTab(selectedTabProp ?? MAIN_TAB_VALUE);
|
|
304
|
+
}
|
|
305
|
+
}, [selectedTabProp]);
|
|
131
306
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
useCache: false
|
|
142
|
-
});
|
|
307
|
+
const mainViewVisible = selectedTab === MAIN_TAB_VALUE;
|
|
308
|
+
|
|
309
|
+
const subcollections = (collection.subcollections ?? []).filter(c => !c.hideFromNavigation);
|
|
310
|
+
const subcollectionsCount = subcollections?.length ?? 0;
|
|
311
|
+
const customViews = collection.entityViews;
|
|
312
|
+
const customViewsCount = customViews?.length ?? 0;
|
|
313
|
+
const autoSave = collection.formAutoSave && !collection.customId;
|
|
314
|
+
|
|
315
|
+
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
|
|
143
316
|
|
|
144
317
|
const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
|
|
145
|
-
|
|
318
|
+
|
|
319
|
+
const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>> | null>(cachedDirtyValues ?? getDataSourceEntityValues(initialResolvedCollection, status, usedEntity));
|
|
146
320
|
|
|
147
321
|
useEffect(() => {
|
|
148
322
|
if (entity)
|
|
149
323
|
setUsedEntity(entity);
|
|
150
324
|
}, [entity]);
|
|
151
325
|
|
|
152
|
-
|
|
153
|
-
if (status === "new") {
|
|
154
|
-
|
|
326
|
+
const canEdit = useMemo(() => {
|
|
327
|
+
if (status === "new" || status === "copy") {
|
|
328
|
+
return true;
|
|
155
329
|
} else {
|
|
156
|
-
|
|
157
|
-
if (usedEntity)
|
|
158
|
-
setReadOnly(!editEnabled);
|
|
330
|
+
return usedEntity ? canEditEntity(collection, authController, path, usedEntity ?? null) : false;
|
|
159
331
|
}
|
|
160
332
|
}, [authController, usedEntity, status]);
|
|
161
333
|
|
|
334
|
+
const readOnly = !canEdit;
|
|
335
|
+
|
|
162
336
|
const onPreSaveHookError = useCallback((e: Error) => {
|
|
163
337
|
setSaving(false);
|
|
164
338
|
snackbarController.open({
|
|
@@ -177,8 +351,20 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
177
351
|
console.error(e);
|
|
178
352
|
}, [snackbarController]);
|
|
179
353
|
|
|
354
|
+
function clearDirtyCache() {
|
|
355
|
+
if (status === "new" || status === "copy") {
|
|
356
|
+
removeEntityFromCache(path + "#new");
|
|
357
|
+
} else {
|
|
358
|
+
removeEntityFromCache(path + "/" + entityId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
180
362
|
const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
|
|
181
363
|
|
|
364
|
+
clearDirtyCache();
|
|
365
|
+
|
|
366
|
+
onValuesModified?.(false);
|
|
367
|
+
|
|
182
368
|
setSaving(false);
|
|
183
369
|
if (!autoSave)
|
|
184
370
|
snackbarController.open({
|
|
@@ -188,26 +374,23 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
188
374
|
|
|
189
375
|
setUsedEntity(updatedEntity);
|
|
190
376
|
setStatus("existing");
|
|
377
|
+
setEntityId(updatedEntity.id);
|
|
191
378
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (closeAfterSave) {
|
|
198
|
-
sideDialogContext.setBlocked(false);
|
|
199
|
-
sideDialogContext.close(true);
|
|
200
|
-
onClose?.();
|
|
201
|
-
} else if (status !== "existing") {
|
|
202
|
-
sideEntityController.replace({
|
|
379
|
+
if (onSaved) {
|
|
380
|
+
onSaved({
|
|
381
|
+
entity: updatedEntity,
|
|
382
|
+
status,
|
|
203
383
|
path,
|
|
204
384
|
entityId: updatedEntity.id,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
collection,
|
|
385
|
+
selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
|
|
386
|
+
collection
|
|
208
387
|
});
|
|
209
388
|
}
|
|
210
389
|
|
|
390
|
+
if (closeAfterSave) {
|
|
391
|
+
onClose?.();
|
|
392
|
+
}
|
|
393
|
+
|
|
211
394
|
};
|
|
212
395
|
|
|
213
396
|
const onSaveFailure = useCallback((e: Error) => {
|
|
@@ -238,7 +421,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
238
421
|
closeAfterSave: boolean,
|
|
239
422
|
}) => {
|
|
240
423
|
setSaving(true);
|
|
241
|
-
saveEntityWithCallbacks({
|
|
424
|
+
return saveEntityWithCallbacks({
|
|
242
425
|
path,
|
|
243
426
|
entityId,
|
|
244
427
|
values,
|
|
@@ -269,7 +452,7 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
269
452
|
if (autoSave) {
|
|
270
453
|
setValuesToBeSaved(values);
|
|
271
454
|
} else {
|
|
272
|
-
saveEntity({
|
|
455
|
+
return saveEntity({
|
|
273
456
|
collection,
|
|
274
457
|
path,
|
|
275
458
|
entityId,
|
|
@@ -280,33 +463,171 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
280
463
|
}
|
|
281
464
|
};
|
|
282
465
|
|
|
466
|
+
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
467
|
+
|
|
468
|
+
if (mustSetCustomId && !entityId) {
|
|
469
|
+
console.error("Missing custom Id");
|
|
470
|
+
setEntityIdError(true);
|
|
471
|
+
formexController.setSubmitting(false);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setSavingError(undefined);
|
|
476
|
+
setEntityIdError(false);
|
|
477
|
+
|
|
478
|
+
if (status === "existing") {
|
|
479
|
+
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
480
|
+
} else if (status === "new" || status === "copy") {
|
|
481
|
+
if (inputCollection.customId) {
|
|
482
|
+
if (inputCollection.customId !== "optional" && !entityId) {
|
|
483
|
+
throw Error("Form misconfiguration when saving, entityId should be set");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
throw Error("New FormType added, check EntityForm");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return save(values)
|
|
491
|
+
?.then(_ => {
|
|
492
|
+
formexController.resetForm({
|
|
493
|
+
values,
|
|
494
|
+
submitCount: 0,
|
|
495
|
+
touched: {}
|
|
496
|
+
});
|
|
497
|
+
})
|
|
498
|
+
.finally(() => {
|
|
499
|
+
formexController.setSubmitting(false);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const formex: FormexController<M> = useCreateFormex<M>({
|
|
505
|
+
initialValues: baseDataSourceValuesRef.current as M,
|
|
506
|
+
initialDirty: Boolean(cachedDirtyValues),
|
|
507
|
+
onSubmit,
|
|
508
|
+
validation: (values) => {
|
|
509
|
+
return validationSchema?.validate(values, { abortEarly: false })
|
|
510
|
+
.then(() => {
|
|
511
|
+
return {};
|
|
512
|
+
})
|
|
513
|
+
.catch((e: any) => {
|
|
514
|
+
// const errors: Record<string, string> = {};
|
|
515
|
+
// e.inner.forEach((error: any) => {
|
|
516
|
+
// errors[error.path] = error.message;
|
|
517
|
+
// });
|
|
518
|
+
return yupToFormErrors(e);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const resolvedCollection = resolveCollection<M>({
|
|
524
|
+
collection: inputCollection,
|
|
525
|
+
path,
|
|
526
|
+
entityId,
|
|
527
|
+
values: formex.values,
|
|
528
|
+
previousValues: formex.initialValues,
|
|
529
|
+
propertyConfigs: customizationController.propertyConfigs
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
|
|
533
|
+
|
|
534
|
+
const save = (values: EntityValues<M>): Promise<void> => {
|
|
535
|
+
lastSavedValues.current = values;
|
|
536
|
+
return onSaveEntityRequest({
|
|
537
|
+
collection: resolvedCollection,
|
|
538
|
+
path,
|
|
539
|
+
entityId,
|
|
540
|
+
values,
|
|
541
|
+
previousValues: entity?.values,
|
|
542
|
+
closeAfterSave: closeAfterSaveRef.current,
|
|
543
|
+
autoSave: autoSave ?? false
|
|
544
|
+
}).then(_ => {
|
|
545
|
+
const eventName: CMSAnalyticsEvent = status === "new"
|
|
546
|
+
? "new_entity_saved"
|
|
547
|
+
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
548
|
+
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
549
|
+
}).catch(e => {
|
|
550
|
+
console.error(e);
|
|
551
|
+
setSavingError(e);
|
|
552
|
+
}).finally(() => {
|
|
553
|
+
closeAfterSaveRef.current = false;
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const formContext: FormContext<M> = {
|
|
558
|
+
// @ts-ignore
|
|
559
|
+
setFieldValue: useCallback(formex.setFieldValue, []),
|
|
560
|
+
values: formex.values,
|
|
561
|
+
collection: resolvedCollection,
|
|
562
|
+
entityId: entityId as string,
|
|
563
|
+
path,
|
|
564
|
+
save,
|
|
565
|
+
formex
|
|
566
|
+
};
|
|
567
|
+
|
|
283
568
|
const resolvedEntityViews = customViews ? customViews
|
|
284
569
|
.map(e => resolveEntityView(e, customizationController.entityViews))
|
|
285
570
|
.filter(Boolean) as EntityCustomView[]
|
|
286
571
|
: [];
|
|
287
572
|
|
|
288
|
-
const
|
|
573
|
+
const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
|
|
574
|
+
const shouldShowEntityActions = !readOnly && (selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
|
|
575
|
+
const actionsAtTheBottom = !largeLayout || layout === "side_panel" || shouldShowEntityActions === "bottom";
|
|
576
|
+
|
|
577
|
+
const secondaryForms: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
578
|
+
.filter(e => e.includeActions)
|
|
289
579
|
.map(
|
|
290
580
|
(customView, colIndex) => {
|
|
291
581
|
if (!customView)
|
|
292
582
|
return null;
|
|
293
|
-
|
|
583
|
+
const Builder = customView.Builder;
|
|
584
|
+
if (!Builder) {
|
|
585
|
+
console.error("customView.Builder is not defined");
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return <FormLayout
|
|
590
|
+
key={`custom_view_${customView.key}`}
|
|
591
|
+
className={selectedTab !== customView.key ? "hidden" : ""}
|
|
592
|
+
formex={formex}>
|
|
593
|
+
<ErrorBoundary>
|
|
594
|
+
{formContext && <Builder
|
|
595
|
+
collection={collection}
|
|
596
|
+
entity={usedEntity}
|
|
597
|
+
modifiedValues={formex.values ?? usedEntity?.values}
|
|
598
|
+
formContext={formContext}
|
|
599
|
+
/>}
|
|
600
|
+
</ErrorBoundary>
|
|
601
|
+
</FormLayout>
|
|
602
|
+
|
|
603
|
+
}
|
|
604
|
+
).filter(Boolean);
|
|
605
|
+
|
|
606
|
+
const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
607
|
+
.filter(e => !e.includeActions)
|
|
608
|
+
.map(
|
|
609
|
+
(customView, colIndex) => {
|
|
610
|
+
if (!customView)
|
|
294
611
|
return null;
|
|
295
612
|
const Builder = customView.Builder;
|
|
296
613
|
if (!Builder) {
|
|
297
614
|
console.error("customView.Builder is not defined");
|
|
298
615
|
return null;
|
|
299
616
|
}
|
|
617
|
+
|
|
300
618
|
return <div
|
|
301
|
-
className={
|
|
302
|
-
"relative flex-
|
|
619
|
+
className={cls(defaultBorderMixin,
|
|
620
|
+
"relative flex-1 w-full h-full overflow-auto",
|
|
621
|
+
{
|
|
622
|
+
"hidden": selectedTab !== customView.key
|
|
623
|
+
})}
|
|
303
624
|
key={`custom_view_${customView.key}`}
|
|
304
625
|
role="tabpanel">
|
|
305
626
|
<ErrorBoundary>
|
|
306
627
|
{formContext && <Builder
|
|
307
628
|
collection={collection}
|
|
308
629
|
entity={usedEntity}
|
|
309
|
-
modifiedValues={
|
|
630
|
+
modifiedValues={formex.values ?? usedEntity?.values}
|
|
310
631
|
formContext={formContext}
|
|
311
632
|
/>}
|
|
312
633
|
</ErrorBoundary>
|
|
@@ -317,21 +638,18 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
317
638
|
const globalLoading = (dataLoading && !usedEntity) ||
|
|
318
639
|
((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
|
|
319
640
|
|
|
320
|
-
const loading = globalLoading || saving;
|
|
321
|
-
|
|
322
641
|
const subCollectionsViews = subcollections && subcollections.map(
|
|
323
642
|
(subcollection, colIndex) => {
|
|
324
643
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
325
644
|
const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
326
|
-
if (
|
|
327
|
-
return null;
|
|
645
|
+
if (selectedTab !== subcollectionId) return null;
|
|
328
646
|
return (
|
|
329
647
|
<div
|
|
330
|
-
className={"relative flex-
|
|
648
|
+
className={"relative flex-1 h-full overflow-auto w-full"}
|
|
331
649
|
key={`subcol_${subcollectionId}`}
|
|
332
650
|
role="tabpanel">
|
|
333
651
|
|
|
334
|
-
{
|
|
652
|
+
{globalLoading || saving && <CircularProgressCenter/>}
|
|
335
653
|
|
|
336
654
|
{!globalLoading &&
|
|
337
655
|
(usedEntity && fullPath
|
|
@@ -339,7 +657,9 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
339
657
|
fullPath={fullPath}
|
|
340
658
|
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
341
659
|
isSubCollection={true}
|
|
342
|
-
{
|
|
660
|
+
updateUrl={false}
|
|
661
|
+
{...subcollection}
|
|
662
|
+
openEntityMode={layout}/>
|
|
343
663
|
: <div
|
|
344
664
|
className="flex items-center justify-center w-full h-full p-3">
|
|
345
665
|
<Typography variant={"label"}>
|
|
@@ -355,25 +675,21 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
355
675
|
).filter(Boolean);
|
|
356
676
|
|
|
357
677
|
const onDiscard = useCallback(() => {
|
|
358
|
-
|
|
678
|
+
onValuesModified?.(false);
|
|
359
679
|
}, []);
|
|
360
680
|
|
|
361
681
|
const onSideTabClick = (value: string) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
682
|
+
setSelectedTab(value);
|
|
683
|
+
if (status === "existing") {
|
|
684
|
+
onTabChange?.({
|
|
685
|
+
path,
|
|
686
|
+
entityId,
|
|
687
|
+
selectedTab: value === MAIN_TAB_VALUE ? undefined : value,
|
|
688
|
+
collection
|
|
689
|
+
});
|
|
690
|
+
}
|
|
370
691
|
};
|
|
371
692
|
|
|
372
|
-
const onValuesChanged = useCallback((values?: EntityValues<M>) => {
|
|
373
|
-
modifiedValuesRef.current = values;
|
|
374
|
-
}, []);
|
|
375
|
-
|
|
376
|
-
// eslint-disable-next-line n/handle-callback-err
|
|
377
693
|
const onIdUpdateError = useCallback((error: any) => {
|
|
378
694
|
snackbarController.open({
|
|
379
695
|
type: "error",
|
|
@@ -390,74 +706,315 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
390
706
|
: undefined);
|
|
391
707
|
}, []);
|
|
392
708
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
709
|
+
const pluginActions: React.ReactNode[] = [];
|
|
710
|
+
|
|
711
|
+
const plugins = customizationController.plugins;
|
|
712
|
+
|
|
713
|
+
if (plugins && inputCollection) {
|
|
714
|
+
const actionProps: PluginFormActionProps = {
|
|
715
|
+
entityId,
|
|
716
|
+
path,
|
|
717
|
+
status,
|
|
718
|
+
collection: inputCollection,
|
|
719
|
+
context,
|
|
720
|
+
currentEntityId: entityId,
|
|
721
|
+
formContext,
|
|
722
|
+
layout
|
|
723
|
+
};
|
|
724
|
+
pluginActions.push(...plugins.map((plugin, i) => (
|
|
725
|
+
plugin.form?.Actions
|
|
726
|
+
? <plugin.form.Actions
|
|
727
|
+
key={`actions_${plugin.key}`} {...actionProps}/>
|
|
728
|
+
: null
|
|
729
|
+
)).filter(Boolean));
|
|
396
730
|
}
|
|
397
731
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
732
|
+
const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
|
|
733
|
+
const title = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
|
|
734
|
+
|
|
735
|
+
const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
|
|
736
|
+
|
|
737
|
+
const doOnIdUpdate = useCallback(async () => {
|
|
738
|
+
if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
|
|
739
|
+
setCustomIdLoading(true);
|
|
740
|
+
try {
|
|
741
|
+
const updatedId = await onIdUpdate({
|
|
742
|
+
collection: resolvedCollection,
|
|
743
|
+
path,
|
|
744
|
+
entityId,
|
|
745
|
+
values: formex.values,
|
|
746
|
+
context
|
|
747
|
+
});
|
|
748
|
+
setEntityId(updatedId);
|
|
749
|
+
} catch (e) {
|
|
750
|
+
onIdUpdateError?.(e);
|
|
751
|
+
console.error(e);
|
|
752
|
+
}
|
|
753
|
+
setCustomIdLoading(false);
|
|
754
|
+
}
|
|
755
|
+
}, [entityId, formex.values, status]);
|
|
756
|
+
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
doOnIdUpdate();
|
|
759
|
+
}, [doOnIdUpdate]);
|
|
760
|
+
|
|
761
|
+
const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
762
|
+
|
|
763
|
+
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
764
|
+
name,
|
|
765
|
+
value,
|
|
766
|
+
property
|
|
767
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
768
|
+
[dataSource, path, entityId]);
|
|
769
|
+
|
|
770
|
+
const validationSchema = useMemo(() => entityId
|
|
771
|
+
? getYupEntitySchema(
|
|
772
|
+
entityId,
|
|
773
|
+
resolvedCollection.properties,
|
|
774
|
+
uniqueFieldValidator)
|
|
775
|
+
: undefined,
|
|
776
|
+
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
777
|
+
|
|
778
|
+
const getActionsForEntity = useCallback(({
|
|
779
|
+
entity,
|
|
780
|
+
customEntityActions
|
|
781
|
+
}: {
|
|
782
|
+
entity?: Entity<M>,
|
|
783
|
+
customEntityActions?: EntityAction[]
|
|
784
|
+
}): EntityAction[] => {
|
|
785
|
+
const createEnabled = canCreateEntity(inputCollection, authController, path, null);
|
|
786
|
+
const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
|
|
787
|
+
const actions: EntityAction[] = [];
|
|
788
|
+
if (createEnabled)
|
|
789
|
+
actions.push(copyEntityAction);
|
|
790
|
+
if (deleteEnabled)
|
|
791
|
+
actions.push(deleteEntityAction);
|
|
792
|
+
if (customEntityActions)
|
|
793
|
+
return mergeEntityActions(actions, customEntityActions);
|
|
794
|
+
return actions;
|
|
795
|
+
}, [authController, inputCollection, path]);
|
|
796
|
+
|
|
797
|
+
const deferredValues = useDeferredValue(formex.values);
|
|
798
|
+
const modified = formex.dirty;
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
802
|
+
if (modified) {
|
|
803
|
+
saveEntityToCache(key, deferredValues);
|
|
804
|
+
}
|
|
805
|
+
}, [deferredValues, modified]);
|
|
806
|
+
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (!autoSave) {
|
|
809
|
+
onValuesModified?.(modified);
|
|
810
|
+
}
|
|
811
|
+
}, [modified]);
|
|
812
|
+
|
|
813
|
+
useOnAutoSave(autoSave, formex, lastSavedValues, save);
|
|
814
|
+
|
|
815
|
+
useEffect(() => {
|
|
816
|
+
if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
|
|
817
|
+
// we update the form fields from the Firestore data
|
|
818
|
+
// if they were not touched
|
|
819
|
+
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
820
|
+
const formValue = formex.values[key];
|
|
821
|
+
if (!equal(value, formValue) && !formex.touched[key]) {
|
|
822
|
+
console.debug("Updated value from the datasource:", key, value);
|
|
823
|
+
formex.setFieldValue(key, value !== undefined ? value : null);
|
|
433
824
|
}
|
|
434
825
|
});
|
|
435
826
|
}
|
|
436
|
-
|
|
437
|
-
|
|
827
|
+
}, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
|
|
828
|
+
|
|
829
|
+
const formFieldKeys = getFormFieldKeys(resolvedCollection);
|
|
830
|
+
const resolvedProperties = formFieldKeys.map(key => resolvedCollection.properties[key]);
|
|
831
|
+
|
|
832
|
+
const formFields = (
|
|
833
|
+
<div className={"flex flex-wrap gap-x-4 w-full space-y-8"}>
|
|
834
|
+
{formFieldKeys
|
|
835
|
+
.map((key) => {
|
|
836
|
+
|
|
837
|
+
const property = resolvedCollection.properties[key];
|
|
838
|
+
if (property) {
|
|
839
|
+
|
|
840
|
+
const underlyingValueHasChanged: boolean =
|
|
841
|
+
!!underlyingChanges &&
|
|
842
|
+
Object.keys(underlyingChanges).includes(key) &&
|
|
843
|
+
formex.touched[key];
|
|
844
|
+
|
|
845
|
+
const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
|
|
846
|
+
const hidden = isHidden(property);
|
|
847
|
+
if (hidden) return null;
|
|
848
|
+
const widthPercentage = property.widthPercentage ?? 100;
|
|
849
|
+
const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
|
|
850
|
+
propertyKey: key,
|
|
851
|
+
disabled,
|
|
852
|
+
property,
|
|
853
|
+
includeDescription: property.description || property.longDescription,
|
|
854
|
+
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
855
|
+
context: formContext,
|
|
856
|
+
partOfArray: false,
|
|
857
|
+
minimalistView: false,
|
|
858
|
+
autoFocus: false
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
return (
|
|
862
|
+
<div id={`form_field_${key}`}
|
|
863
|
+
className={"relative"}
|
|
864
|
+
style={{ width: widthPercentage === 100 ? "100%" : `calc(${widthPercentage}% - 8px)` }}
|
|
865
|
+
key={`field_${resolvedCollection.name}_${key}`}>
|
|
866
|
+
<ErrorBoundary>
|
|
867
|
+
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
868
|
+
</ErrorBoundary>
|
|
869
|
+
</div>
|
|
870
|
+
);
|
|
871
|
+
}
|
|
438
872
|
|
|
439
|
-
|
|
873
|
+
const additionalField = resolvedCollection.additionalFields?.find(f => f.key === key);
|
|
874
|
+
if (additionalField && entity) {
|
|
875
|
+
const Builder = additionalField.Builder;
|
|
876
|
+
if (!Builder && !additionalField.value) {
|
|
877
|
+
throw new Error("When using additional fields you need to provide a Builder or a value");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const child = Builder
|
|
881
|
+
? <Builder entity={entity} context={context}/>
|
|
882
|
+
: <div className={"w-full"}>
|
|
883
|
+
{additionalField.value?.({
|
|
884
|
+
entity,
|
|
885
|
+
context
|
|
886
|
+
})?.toString()}
|
|
887
|
+
</div>;
|
|
888
|
+
|
|
889
|
+
return (
|
|
890
|
+
<div key={`additional_${key}`} className={"w-full"}>
|
|
891
|
+
<LabelWithIconAndTooltip
|
|
892
|
+
propertyKey={key}
|
|
893
|
+
icon={<NotesIcon size={"small"}/>}
|
|
894
|
+
title={additionalField.name}
|
|
895
|
+
className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
|
|
896
|
+
<div
|
|
897
|
+
className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
|
|
898
|
+
|
|
899
|
+
<ErrorBoundary>
|
|
900
|
+
{child}
|
|
901
|
+
</ErrorBoundary>
|
|
902
|
+
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
console.warn(`Property ${key} not found in collection ${resolvedCollection.name} in properties or additional fields. Skipping.`);
|
|
909
|
+
return null;
|
|
910
|
+
})
|
|
911
|
+
.filter(Boolean)}
|
|
912
|
+
|
|
913
|
+
</div>);
|
|
914
|
+
|
|
915
|
+
const disabled = formex.isSubmitting || (!modified && status === "existing");
|
|
916
|
+
const formRef = React.useRef<HTMLDivElement>(null);
|
|
917
|
+
|
|
918
|
+
const entityActions = getActionsForEntity({
|
|
919
|
+
entity,
|
|
920
|
+
customEntityActions: inputCollection.entityActions
|
|
921
|
+
});
|
|
922
|
+
const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
|
|
923
|
+
|
|
924
|
+
const dialogActions = actionsAtTheBottom
|
|
925
|
+
? buildBottomActions({
|
|
926
|
+
savingError: savingError,
|
|
927
|
+
entity: entity,
|
|
928
|
+
formActions: formActions,
|
|
929
|
+
resolvedCollection: resolvedCollection,
|
|
930
|
+
context: context,
|
|
931
|
+
sideEntityController: sideEntityController,
|
|
932
|
+
isSubmitting: formex.isSubmitting,
|
|
933
|
+
disabled: disabled,
|
|
934
|
+
status: status,
|
|
935
|
+
setPendingClose: (value: boolean) => {
|
|
936
|
+
closeAfterSaveRef.current = value;
|
|
937
|
+
},
|
|
938
|
+
pluginActions,
|
|
939
|
+
layout
|
|
940
|
+
})
|
|
941
|
+
: buildSideActions({
|
|
942
|
+
savingError: savingError,
|
|
943
|
+
entity: entity,
|
|
944
|
+
formActions: formActions,
|
|
945
|
+
resolvedCollection: resolvedCollection,
|
|
946
|
+
context: context,
|
|
947
|
+
sideEntityController: sideEntityController,
|
|
948
|
+
isSubmitting: formex.isSubmitting,
|
|
949
|
+
disabled: disabled,
|
|
950
|
+
status: status,
|
|
951
|
+
pluginActions,
|
|
952
|
+
layout
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
const entityView = (readOnly === undefined)
|
|
440
956
|
? <></>
|
|
441
957
|
: (!readOnly
|
|
442
|
-
?
|
|
958
|
+
? (
|
|
959
|
+
<ErrorBoundary>
|
|
960
|
+
<>
|
|
961
|
+
<div
|
|
962
|
+
className={"w-full py-2 flex flex-col items-start mt-4 lg:mt-8 mb-8"}>
|
|
963
|
+
|
|
964
|
+
<Typography
|
|
965
|
+
className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
|
|
966
|
+
variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
|
|
967
|
+
</Typography>
|
|
968
|
+
<Alert color={"base"} className={"w-full"} size={"small"}>
|
|
969
|
+
<code
|
|
970
|
+
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>{entity?.path ?? path}/{entityId}</code>
|
|
971
|
+
</Alert>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
{!collection.hideIdFromForm &&
|
|
975
|
+
<CustomIdField customId={inputCollection.customId}
|
|
976
|
+
entityId={entityId}
|
|
977
|
+
status={status}
|
|
978
|
+
onChange={setEntityId}
|
|
979
|
+
error={entityIdError}
|
|
980
|
+
loading={customIdLoading}
|
|
981
|
+
entity={entity}/>}
|
|
982
|
+
|
|
983
|
+
{entityId && formContext && <>
|
|
984
|
+
<div className="mt-12 flex flex-col gap-8"
|
|
985
|
+
ref={formRef}>
|
|
986
|
+
|
|
987
|
+
{formFields}
|
|
988
|
+
|
|
989
|
+
<ErrorFocus containerRef={formRef}/>
|
|
990
|
+
|
|
991
|
+
</div>
|
|
992
|
+
</>}
|
|
993
|
+
|
|
994
|
+
{actionsAtTheBottom && <div className="h-16"/>}
|
|
995
|
+
|
|
996
|
+
</>
|
|
997
|
+
</ErrorBoundary>
|
|
998
|
+
)
|
|
443
999
|
: (
|
|
444
|
-
|
|
1000
|
+
<div className={"flex flex-col"}>
|
|
445
1001
|
<Typography
|
|
446
1002
|
className={"mt-16 mb-8 mx-8"}
|
|
447
1003
|
variant={"h4"}>{collection.singularName ?? collection.name}
|
|
448
1004
|
</Typography>
|
|
449
1005
|
<EntityView
|
|
450
|
-
className={"px-
|
|
1006
|
+
className={"px-8"}
|
|
451
1007
|
entity={usedEntity as Entity<M>}
|
|
452
1008
|
path={path}
|
|
453
1009
|
collection={collection}/>
|
|
454
|
-
|
|
1010
|
+
|
|
1011
|
+
</div>
|
|
455
1012
|
));
|
|
456
1013
|
|
|
457
1014
|
const subcollectionTabs = subcollections && subcollections.map(
|
|
458
1015
|
(subcollection) =>
|
|
459
1016
|
<Tab
|
|
460
|
-
className="text-sm min-w-[
|
|
1017
|
+
className="text-sm min-w-[120px]"
|
|
461
1018
|
value={subcollection.id}
|
|
462
1019
|
key={`entity_detail_collection_tab_${subcollection.name}`}>
|
|
463
1020
|
{subcollection.name}
|
|
@@ -468,94 +1025,325 @@ export function EntityEditView<M extends Record<string, any>, UserType extends U
|
|
|
468
1025
|
(view) =>
|
|
469
1026
|
|
|
470
1027
|
<Tab
|
|
471
|
-
className="text-sm min-w-[
|
|
1028
|
+
className="text-sm min-w-[120px]"
|
|
472
1029
|
value={view.key}
|
|
473
1030
|
key={`entity_detail_collection_tab_${view.name}`}>
|
|
474
1031
|
{view.name}
|
|
475
1032
|
</Tab>
|
|
476
1033
|
);
|
|
477
1034
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
<>
|
|
1035
|
+
useEffect(() => {
|
|
1036
|
+
if (entityId && onIdChange)
|
|
1037
|
+
onIdChange(entityId);
|
|
1038
|
+
}, [entityId, onIdChange]);
|
|
483
1039
|
|
|
484
|
-
|
|
485
|
-
|
|
1040
|
+
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
|
|
1041
|
+
const shouldIncludeForm = selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions;
|
|
486
1042
|
|
|
487
|
-
|
|
488
|
-
className="pb-1 self-center">
|
|
489
|
-
<IconButton
|
|
490
|
-
onClick={() => {
|
|
491
|
-
onClose?.();
|
|
492
|
-
return sideDialogContext.close(false);
|
|
493
|
-
}}
|
|
494
|
-
size="large">
|
|
495
|
-
<CloseIcon/>
|
|
496
|
-
</IconButton>
|
|
497
|
-
</div>
|
|
1043
|
+
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
498
1044
|
|
|
499
|
-
|
|
1045
|
+
{shouldShowTopBar && <div
|
|
1046
|
+
className={cls("h-14 flex overflow-visible overflow-x-scroll w-full no-scrollbar h-14 border-b pl-2 pr-2 pt-1 flex items-end bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
|
|
500
1047
|
|
|
501
|
-
|
|
502
|
-
className="self-center">
|
|
503
|
-
<CircularProgress size={"small"}/>
|
|
504
|
-
</div>}
|
|
1048
|
+
{barActions}
|
|
505
1049
|
|
|
506
|
-
|
|
507
|
-
value={selectedTabRef.current}
|
|
508
|
-
onValueChange={(value) => {
|
|
509
|
-
onSideTabClick(value);
|
|
510
|
-
}}
|
|
511
|
-
className="pl-4 pr-4 pt-0">
|
|
1050
|
+
<div className={"flex-grow"}/>
|
|
512
1051
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
!hasAdditionalViews ? "hidden" : ""
|
|
518
|
-
} text-sm min-w-[140px]`}
|
|
519
|
-
>{collection.singularName ?? collection.name}</Tab>
|
|
1052
|
+
{globalLoading && <div
|
|
1053
|
+
className="self-center">
|
|
1054
|
+
<CircularProgress size={"small"}/>
|
|
1055
|
+
</div>}
|
|
520
1056
|
|
|
521
|
-
|
|
1057
|
+
{hasAdditionalViews && <Tabs
|
|
1058
|
+
value={selectedTab}
|
|
1059
|
+
onValueChange={(value) => {
|
|
1060
|
+
onSideTabClick(value);
|
|
1061
|
+
}}>
|
|
522
1062
|
|
|
523
|
-
|
|
524
|
-
|
|
1063
|
+
<Tab
|
|
1064
|
+
disabled={!hasAdditionalViews}
|
|
1065
|
+
value={MAIN_TAB_VALUE}
|
|
1066
|
+
className={"text-sm min-w-[120px]"}
|
|
1067
|
+
>{collection.singularName ?? collection.name}</Tab>
|
|
525
1068
|
|
|
526
|
-
|
|
1069
|
+
{customViewTabs}
|
|
527
1070
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
style={{
|
|
531
|
-
// width: `calc(${ADDITIONAL_TAB_WIDTH} + ${resolvedFormWidth})`,
|
|
532
|
-
// maxWidth: "100%",
|
|
533
|
-
// [`@media (max-width: ${resolvedFormWidth})`]: {
|
|
534
|
-
// width: resolvedFormWidth
|
|
535
|
-
// }
|
|
536
|
-
}}>
|
|
1071
|
+
{subcollectionTabs}
|
|
1072
|
+
</Tabs>}
|
|
537
1073
|
|
|
538
|
-
|
|
539
|
-
role="tabpanel"
|
|
540
|
-
hidden={!mainViewVisible}
|
|
541
|
-
id={`form_${path}`}
|
|
542
|
-
className={" w-full"}>
|
|
1074
|
+
</div>}
|
|
543
1075
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1076
|
+
<form
|
|
1077
|
+
onSubmit={formex.handleSubmit}
|
|
1078
|
+
onReset={() => {
|
|
547
1079
|
|
|
548
|
-
|
|
1080
|
+
clearDirtyCache();
|
|
549
1081
|
|
|
550
|
-
|
|
1082
|
+
formex.resetForm({
|
|
1083
|
+
values: getDataSourceEntityValues(initialResolvedCollection, status, entity) as M,
|
|
1084
|
+
});
|
|
551
1085
|
|
|
552
|
-
|
|
1086
|
+
return onDiscard();
|
|
1087
|
+
}}
|
|
1088
|
+
noValidate
|
|
1089
|
+
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center",
|
|
1090
|
+
shouldIncludeForm ? "" : "hidden")}>
|
|
553
1091
|
|
|
1092
|
+
<FormLayout
|
|
1093
|
+
className={!mainViewVisible ? "hidden" : ""}
|
|
1094
|
+
id={`form_${path}`}
|
|
1095
|
+
formex={formex}>
|
|
1096
|
+
{globalLoading
|
|
1097
|
+
? <div className="w-full pt-12 pb-16 px-4 sm:px-8 md:px-10">
|
|
1098
|
+
<CircularProgressCenter/>
|
|
554
1099
|
</div>
|
|
1100
|
+
: entityView}
|
|
1101
|
+
</FormLayout>
|
|
1102
|
+
|
|
1103
|
+
{secondaryForms}
|
|
1104
|
+
|
|
1105
|
+
{shouldShowEntityActions && dialogActions}
|
|
1106
|
+
|
|
1107
|
+
</form>
|
|
1108
|
+
|
|
1109
|
+
{customViewsView}
|
|
1110
|
+
|
|
1111
|
+
{subCollectionsViews}
|
|
555
1112
|
|
|
556
|
-
|
|
1113
|
+
</div>;
|
|
1114
|
+
|
|
1115
|
+
if (plugins) {
|
|
1116
|
+
plugins.forEach((plugin: FireCMSPlugin) => {
|
|
1117
|
+
if (plugin.form?.provider) {
|
|
1118
|
+
result = (
|
|
1119
|
+
<plugin.form.provider.Component
|
|
1120
|
+
status={status}
|
|
1121
|
+
path={path}
|
|
1122
|
+
collection={collection}
|
|
1123
|
+
onDiscard={onDiscard}
|
|
1124
|
+
entity={usedEntity}
|
|
1125
|
+
context={context}
|
|
1126
|
+
formContext={formContext}
|
|
1127
|
+
{...plugin.form.provider.props}>
|
|
1128
|
+
{result}
|
|
1129
|
+
</plugin.form.provider.Component>
|
|
1130
|
+
);
|
|
557
1131
|
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
558
1134
|
|
|
559
|
-
|
|
1135
|
+
return (
|
|
1136
|
+
<Formex value={formex}>
|
|
1137
|
+
|
|
1138
|
+
{result}
|
|
1139
|
+
</Formex>
|
|
560
1140
|
);
|
|
561
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
function getDataSourceEntityValues<M extends object>(collection: ResolvedEntityCollection,
|
|
1144
|
+
status: "new" | "existing" | "copy",
|
|
1145
|
+
entity: Entity<M> | undefined): Partial<EntityValues<M>> {
|
|
1146
|
+
|
|
1147
|
+
const properties = collection.properties;
|
|
1148
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
1149
|
+
return entity.values ?? getDefaultValuesFor(properties);
|
|
1150
|
+
} else if (status === "new") {
|
|
1151
|
+
return getDefaultValuesFor(properties);
|
|
1152
|
+
} else {
|
|
1153
|
+
console.error({
|
|
1154
|
+
status,
|
|
1155
|
+
entity
|
|
1156
|
+
});
|
|
1157
|
+
throw new Error("Form has not been initialised with the correct parameters");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
1162
|
+
collection: ResolvedEntityCollection<M>,
|
|
1163
|
+
path: string,
|
|
1164
|
+
entityId: string | undefined,
|
|
1165
|
+
values: EntityValues<M>,
|
|
1166
|
+
previousValues?: EntityValues<M>,
|
|
1167
|
+
closeAfterSave: boolean,
|
|
1168
|
+
autoSave: boolean
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
|
|
1172
|
+
let errors: Record<string, any> = {};
|
|
1173
|
+
if (yupError.inner) {
|
|
1174
|
+
if (yupError.inner.length === 0) {
|
|
1175
|
+
return setIn(errors, yupError.path!, yupError.message);
|
|
1176
|
+
}
|
|
1177
|
+
for (const err of yupError.inner) {
|
|
1178
|
+
if (!getIn(errors, err.path!)) {
|
|
1179
|
+
errors = setIn(errors, err.path!, err.message);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return errors;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
type ActionsViewProps<M extends object> = {
|
|
1187
|
+
savingError: Error | undefined,
|
|
1188
|
+
entity: Entity<M> | undefined,
|
|
1189
|
+
formActions: EntityAction[],
|
|
1190
|
+
resolvedCollection: ResolvedEntityCollection,
|
|
1191
|
+
context: FireCMSContext,
|
|
1192
|
+
sideEntityController: SideEntityController,
|
|
1193
|
+
isSubmitting: boolean,
|
|
1194
|
+
disabled: boolean,
|
|
1195
|
+
status: "new" | "existing" | "copy",
|
|
1196
|
+
setPendingClose?: (value: boolean) => void,
|
|
1197
|
+
pluginActions?: React.ReactNode[],
|
|
1198
|
+
layout: "side_panel" | "full_screen";
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
function buildBottomActions<M extends object>({
|
|
1202
|
+
savingError,
|
|
1203
|
+
entity,
|
|
1204
|
+
formActions,
|
|
1205
|
+
resolvedCollection,
|
|
1206
|
+
context,
|
|
1207
|
+
sideEntityController,
|
|
1208
|
+
isSubmitting,
|
|
1209
|
+
disabled,
|
|
1210
|
+
status,
|
|
1211
|
+
setPendingClose,
|
|
1212
|
+
pluginActions,
|
|
1213
|
+
layout
|
|
1214
|
+
}: ActionsViewProps<M>) {
|
|
1215
|
+
|
|
1216
|
+
const canClose = layout === "side_panel";
|
|
1217
|
+
return <DialogActions position={"absolute"}>
|
|
1218
|
+
|
|
1219
|
+
{savingError &&
|
|
1220
|
+
<div className="text-right">
|
|
1221
|
+
<Typography color={"error"}>
|
|
1222
|
+
{savingError.message}
|
|
1223
|
+
</Typography>
|
|
1224
|
+
</div>}
|
|
1225
|
+
|
|
1226
|
+
{entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
|
|
1227
|
+
{formActions.map(action => (
|
|
1228
|
+
<IconButton
|
|
1229
|
+
key={action.name}
|
|
1230
|
+
color="primary"
|
|
1231
|
+
onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
1232
|
+
event.stopPropagation();
|
|
1233
|
+
if (entity)
|
|
1234
|
+
action.onClick({
|
|
1235
|
+
entity,
|
|
1236
|
+
fullPath: resolvedCollection.path,
|
|
1237
|
+
collection: resolvedCollection,
|
|
1238
|
+
context,
|
|
1239
|
+
sideEntityController,
|
|
1240
|
+
openEntityMode: layout
|
|
1241
|
+
});
|
|
1242
|
+
}}>
|
|
1243
|
+
{action.icon}
|
|
1244
|
+
</IconButton>
|
|
1245
|
+
))}
|
|
1246
|
+
</div>}
|
|
1247
|
+
|
|
1248
|
+
{pluginActions}
|
|
1249
|
+
|
|
1250
|
+
<Button
|
|
1251
|
+
variant="text"
|
|
1252
|
+
disabled={disabled || isSubmitting}
|
|
1253
|
+
type="reset">
|
|
1254
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
1255
|
+
</Button>
|
|
1256
|
+
|
|
1257
|
+
<Button
|
|
1258
|
+
variant={canClose ? "text" : "filled"}
|
|
1259
|
+
color="primary"
|
|
1260
|
+
type="submit"
|
|
1261
|
+
disabled={disabled || isSubmitting}
|
|
1262
|
+
onClick={() => {
|
|
1263
|
+
setPendingClose?.(false);
|
|
1264
|
+
}}>
|
|
1265
|
+
{status === "existing" && "Save"}
|
|
1266
|
+
{status === "copy" && "Create copy"}
|
|
1267
|
+
{status === "new" && "Create"}
|
|
1268
|
+
</Button>
|
|
1269
|
+
|
|
1270
|
+
{canClose && <LoadingButton
|
|
1271
|
+
variant="filled"
|
|
1272
|
+
color="primary"
|
|
1273
|
+
type="submit"
|
|
1274
|
+
loading={isSubmitting}
|
|
1275
|
+
disabled={disabled}
|
|
1276
|
+
onClick={() => {
|
|
1277
|
+
setPendingClose?.(true);
|
|
1278
|
+
}}>
|
|
1279
|
+
{status === "existing" && "Save and close"}
|
|
1280
|
+
{status === "copy" && "Create copy and close"}
|
|
1281
|
+
{status === "new" && "Create and close"}
|
|
1282
|
+
</LoadingButton>}
|
|
1283
|
+
|
|
1284
|
+
</DialogActions>;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function buildSideActions<M extends object>({
|
|
1288
|
+
savingError,
|
|
1289
|
+
entity,
|
|
1290
|
+
formActions,
|
|
1291
|
+
resolvedCollection,
|
|
1292
|
+
context,
|
|
1293
|
+
sideEntityController,
|
|
1294
|
+
isSubmitting,
|
|
1295
|
+
disabled,
|
|
1296
|
+
status,
|
|
1297
|
+
setPendingClose,
|
|
1298
|
+
pluginActions
|
|
1299
|
+
}: ActionsViewProps<M>) {
|
|
1300
|
+
|
|
1301
|
+
return <div
|
|
1302
|
+
className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin)}>
|
|
1303
|
+
|
|
1304
|
+
<LoadingButton
|
|
1305
|
+
fullWidth={true}
|
|
1306
|
+
variant="filled"
|
|
1307
|
+
color="primary"
|
|
1308
|
+
type="submit"
|
|
1309
|
+
size={"large"}
|
|
1310
|
+
disabled={disabled || isSubmitting}
|
|
1311
|
+
onClick={() => {
|
|
1312
|
+
setPendingClose?.(false);
|
|
1313
|
+
}}>
|
|
1314
|
+
{status === "existing" && "Save"}
|
|
1315
|
+
{status === "copy" && "Create copy"}
|
|
1316
|
+
{status === "new" && "Create"}
|
|
1317
|
+
</LoadingButton>
|
|
1318
|
+
|
|
1319
|
+
<Button
|
|
1320
|
+
fullWidth={true}
|
|
1321
|
+
variant="text"
|
|
1322
|
+
disabled={disabled || isSubmitting}
|
|
1323
|
+
type="reset">
|
|
1324
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
1325
|
+
</Button>
|
|
1326
|
+
|
|
1327
|
+
{pluginActions}
|
|
1328
|
+
|
|
1329
|
+
{savingError &&
|
|
1330
|
+
<div className="text-right">
|
|
1331
|
+
<Typography color={"error"}>
|
|
1332
|
+
{savingError.message}
|
|
1333
|
+
</Typography>
|
|
1334
|
+
</div>}
|
|
1335
|
+
|
|
1336
|
+
</div>;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: any, save: (values: EntityValues<any>) => Promise<void>) {
|
|
1340
|
+
if (!autoSave) return;
|
|
1341
|
+
useEffect(() => {
|
|
1342
|
+
if (autoSave) {
|
|
1343
|
+
if (formex.values && !equal(formex.values, lastSavedValues.current)) {
|
|
1344
|
+
save(formex.values);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}, [formex.values]);
|
|
1348
|
+
}
|
|
1349
|
+
|