@firecms/core 3.0.0-canary.29 → 3.0.0-canary.292
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 +34 -0
- package/dist/app/index.d.ts +4 -0
- package/dist/app/useApp.d.ts +16 -0
- package/dist/components/ArrayContainer.d.ts +31 -12
- 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 +14 -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 +20 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
- package/dist/components/EntityCollectionView/utils.d.ts +3 -0
- package/dist/components/EntityJsonPreview.d.ts +3 -0
- package/dist/components/EntityPreview.d.ts +10 -7
- package/dist/components/ErrorView.d.ts +1 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
- package/dist/components/HomePage/HomePageDnD.d.ts +77 -0
- package/dist/components/HomePage/NavigationCard.d.ts +3 -1
- package/dist/components/HomePage/NavigationCardBinding.d.ts +4 -3
- package/dist/components/HomePage/NavigationGroup.d.ts +8 -1
- package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
- package/dist/components/PropertyCollectionView.d.ts +23 -0
- 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 +14 -4
- package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +2 -1
- package/dist/components/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +24 -12
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- 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 +5 -2
- package/dist/contexts/BreacrumbsContext.d.ts +8 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/DefaultAppBar.d.ts +29 -0
- package/dist/core/DefaultDrawer.d.ts +19 -0
- package/dist/core/DrawerNavigationItem.d.ts +10 -0
- package/dist/core/EntityEditView.d.ts +49 -11
- package/dist/core/EntityEditViewFormActions.d.ts +2 -0
- package/dist/core/FireCMS.d.ts +2 -3
- package/dist/core/FireCMSRouter.d.ts +4 -0
- package/dist/core/NavigationRoutes.d.ts +2 -3
- package/dist/core/SideDialogs.d.ts +4 -2
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/core/index.d.ts +4 -4
- package/dist/form/EntityForm.d.ts +40 -64
- package/dist/form/EntityFormActions.d.ts +21 -0
- 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/FormEntry.d.ts +6 -0
- package/dist/form/components/FormLayout.d.ts +5 -0
- package/dist/form/components/LabelWithIcon.d.ts +1 -1
- package/dist/form/components/LabelWithIconAndTooltip.d.ts +15 -0
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/components/StorageItemPreview.d.ts +4 -4
- package/dist/form/components/index.d.ts +3 -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/ReferenceAsStringFieldBinding.d.ts +9 -0
- 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 +5 -13
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -2
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/form/index.d.ts +18 -18
- 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 +3 -0
- package/dist/hooks/useAuthController.d.ts +1 -1
- package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
- package/dist/hooks/useBuildNavigationController.d.ts +57 -13
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useFireCMSContext.d.ts +1 -1
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/hooks/useModeController.d.ts +1 -2
- 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 +24546 -13965
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +27256 -588
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +3 -17
- package/dist/internal/useBuildSideEntityController.d.ts +3 -3
- 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 +4 -3
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- 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/analytics.d.ts +1 -1
- package/dist/types/auth.d.ts +8 -10
- package/dist/types/collections.d.ts +123 -25
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/datasource.d.ts +52 -36
- package/dist/types/dialogs_controller.d.ts +7 -3
- package/dist/types/entities.d.ts +12 -3
- package/dist/types/entity_actions.d.ts +72 -8
- 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 +79 -39
- package/dist/types/firecms.d.ts +31 -3
- package/dist/types/firecms_context.d.ts +17 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/navigation.d.ts +62 -19
- package/dist/types/permissions.d.ts +4 -4
- package/dist/types/plugins.d.ts +58 -13
- package/dist/types/properties.d.ts +122 -31
- package/dist/types/property_config.d.ts +1 -3
- package/dist/types/roles.d.ts +3 -0
- package/dist/types/side_dialogs_controller.d.ts +10 -0
- package/dist/types/side_entity_controller.d.ts +14 -1
- package/dist/types/storage.d.ts +75 -0
- package/dist/types/user.d.ts +2 -1
- package/dist/util/builders.d.ts +3 -3
- package/dist/util/callbacks.d.ts +2 -0
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entities.d.ts +3 -3
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +28 -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 +7 -4
- package/dist/util/index.d.ts +3 -0
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/navigation_from_path.d.ts +10 -1
- package/dist/util/navigation_utils.d.ts +15 -3
- package/dist/util/objects.d.ts +3 -1
- package/dist/util/permissions.d.ts +4 -4
- package/dist/util/plurals.d.ts +0 -2
- package/dist/util/property_utils.d.ts +4 -4
- package/dist/util/references.d.ts +2 -2
- package/dist/util/resolutions.d.ts +42 -17
- package/dist/util/storage.d.ts +23 -2
- package/dist/util/useStorageUploadController.d.ts +4 -3
- package/package.json +70 -53
- package/src/app/AppBar.tsx +18 -0
- package/src/app/Drawer.tsx +24 -0
- package/src/app/Scaffold.tsx +253 -0
- package/src/app/index.ts +4 -0
- package/src/app/useApp.tsx +32 -0
- package/src/components/ArrayContainer.tsx +447 -229
- package/src/components/CircularProgressCenter.tsx +2 -2
- package/src/components/ClearFilterSortButton.tsx +41 -0
- package/src/components/{DeleteConfirmationDialog.tsx → ConfirmationDialog.tsx} +12 -11
- package/src/components/DeleteEntityDialog.tsx +13 -20
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +87 -62
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +38 -31
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +30 -9
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +84 -42
- 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 +34 -39
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +49 -36
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +20 -8
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +135 -105
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +241 -119
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +7 -4
- 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/EntityJsonPreview.tsx +66 -0
- package/src/components/EntityPreview.tsx +83 -62
- package/src/components/EntityView.tsx +34 -42
- package/src/components/ErrorView.tsx +4 -4
- package/src/components/FireCMSLogo.tsx +7 -51
- package/src/components/HomePage/DefaultHomePage.tsx +516 -158
- package/src/components/HomePage/FavouritesView.tsx +9 -14
- package/src/components/HomePage/HomePageDnD.tsx +702 -0
- package/src/components/HomePage/NavigationCard.tsx +48 -39
- package/src/components/HomePage/NavigationCardBinding.tsx +17 -16
- package/src/components/HomePage/NavigationGroup.tsx +144 -30
- package/src/components/HomePage/RenameGroupDialog.tsx +123 -0
- package/src/components/HomePage/SmallNavigationCard.tsx +5 -6
- package/src/components/NotFoundPage.tsx +2 -2
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/PropertyConfigBadge.tsx +10 -4
- package/src/components/PropertyIdCopyTooltip.tsx +47 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +23 -13
- package/src/components/ReferenceWidget.tsx +21 -11
- package/src/components/SearchIconsView.tsx +10 -7
- package/src/components/SelectableTable/SelectableTable.tsx +157 -157
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +2 -3
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +27 -9
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +36 -12
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +92 -24
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/VirtualTable.tsx +105 -51
- 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 +28 -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 +2 -2
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +2 -1
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +16 -28
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/VirtualTable/types.tsx +2 -3
- package/src/components/{EntityCollectionTable/internal → common}/default_entity_actions.tsx +64 -44
- 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 +16 -2
- 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 +6 -2
- package/src/contexts/BreacrumbsContext.tsx +38 -0
- package/src/contexts/DialogsProvider.tsx +5 -4
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/contexts/ModeController.tsx +1 -3
- package/src/contexts/SnackbarProvider.tsx +2 -0
- package/src/core/DefaultAppBar.tsx +219 -0
- package/src/core/DefaultDrawer.tsx +185 -0
- package/src/core/DrawerNavigationItem.tsx +66 -0
- package/src/core/EntityEditView.tsx +447 -469
- package/src/core/EntityEditViewFormActions.tsx +344 -0
- package/src/core/EntitySidePanel.tsx +96 -23
- package/src/core/FireCMS.tsx +85 -60
- package/src/core/FireCMSRouter.tsx +17 -0
- package/src/core/NavigationRoutes.tsx +28 -38
- package/src/core/SideDialogs.tsx +22 -12
- package/src/core/field_configs.tsx +41 -14
- package/src/core/index.tsx +6 -5
- package/src/form/EntityForm.tsx +740 -523
- package/src/form/EntityFormActions.tsx +226 -0
- package/src/form/PropertyFieldBinding.tsx +88 -41
- package/src/form/components/CustomIdField.tsx +9 -3
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/FieldHelperText.tsx +4 -4
- package/src/form/components/FormEntry.tsx +22 -0
- package/src/form/components/FormLayout.tsx +16 -0
- package/src/form/components/LabelWithIcon.tsx +30 -19
- package/src/form/components/LabelWithIconAndTooltip.tsx +28 -0
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/components/StorageItemPreview.tsx +23 -13
- package/src/form/components/StorageUploadProgress.tsx +5 -6
- package/src/form/components/index.tsx +3 -1
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +34 -19
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +50 -36
- package/src/form/field_bindings/BlockFieldBinding.tsx +56 -33
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +18 -14
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +61 -52
- package/src/form/field_bindings/MapFieldBinding.tsx +73 -55
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +159 -0
- package/src/form/field_bindings/{MultiSelectBinding.tsx → MultiSelectFieldBinding.tsx} +26 -21
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +11 -16
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +42 -31
- package/src/form/field_bindings/RepeatFieldBinding.tsx +62 -35
- package/src/form/field_bindings/SelectFieldBinding.tsx +24 -15
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +257 -199
- package/src/form/field_bindings/SwitchFieldBinding.tsx +29 -24
- package/src/form/field_bindings/TextFieldBinding.tsx +28 -24
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/form/index.tsx +21 -37
- package/src/form/useClearRestoreValue.tsx +2 -2
- package/src/form/validation.ts +13 -23
- package/src/hooks/data/delete.ts +6 -5
- package/src/hooks/data/save.ts +26 -33
- 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 +4 -0
- package/src/hooks/useAuthController.tsx +1 -1
- package/src/hooks/useBreadcrumbsController.tsx +31 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +8 -10
- package/src/hooks/useBuildModeController.tsx +22 -29
- package/src/hooks/useBuildNavigationController.tsx +515 -121
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +9 -35
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/hooks/useLargeLayout.tsx +0 -35
- package/src/hooks/useModeController.tsx +1 -2
- 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 +79 -85
- package/src/internal/useBuildSideDialogsController.tsx +4 -2
- package/src/internal/useBuildSideEntityController.tsx +204 -77
- package/src/internal/useUnsavedChangesDialog.tsx +127 -91
- package/src/preview/PropertyPreview.tsx +42 -25
- 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 +26 -37
- package/src/preview/components/ReferencePreview.tsx +30 -38
- package/src/preview/components/StorageThumbnail.tsx +5 -1
- package/src/preview/components/UrlComponentPreview.tsx +60 -28
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +6 -6
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +7 -5
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +5 -4
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +4 -4
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +7 -6
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +8 -7
- package/src/preview/property_previews/MapPropertyPreview.tsx +14 -13
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +13 -13
- 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 +246 -0
- package/src/routes/HomePageRoute.tsx +17 -0
- package/src/types/analytics.ts +3 -0
- package/src/types/auth.tsx +9 -13
- package/src/types/collections.ts +146 -30
- package/src/types/customization_controller.tsx +9 -1
- package/src/types/datasource.ts +61 -43
- package/src/types/dialogs_controller.tsx +7 -3
- package/src/types/entities.ts +19 -3
- package/src/types/entity_actions.tsx +86 -10
- 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 +91 -42
- package/src/types/firecms.tsx +34 -4
- package/src/types/firecms_context.tsx +18 -1
- package/src/types/index.ts +1 -1
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/navigation.ts +77 -24
- package/src/types/permissions.ts +5 -5
- package/src/types/plugins.tsx +69 -15
- package/src/types/properties.ts +141 -33
- package/src/types/property_config.tsx +2 -2
- package/src/types/roles.ts +3 -0
- package/src/types/side_dialogs_controller.tsx +15 -0
- package/src/types/side_entity_controller.tsx +16 -1
- package/src/types/storage.ts +83 -1
- package/src/types/user.ts +3 -1
- package/src/util/builders.ts +10 -8
- package/src/util/callbacks.ts +119 -0
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entities.ts +11 -8
- package/src/util/entity_actions.ts +28 -0
- package/src/util/entity_cache.ts +223 -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 +36 -11
- package/src/util/index.ts +3 -0
- package/src/util/join_collections.ts +11 -4
- package/src/util/make_properties_editable.ts +5 -19
- package/src/util/navigation_from_path.ts +33 -12
- package/src/util/navigation_utils.ts +141 -25
- package/src/util/objects.ts +128 -33
- 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 +17 -6
- package/src/util/references.ts +19 -8
- package/src/util/resolutions.ts +122 -48
- package/src/util/storage.ts +79 -21
- package/src/util/strings.ts +2 -2
- package/src/util/useStorageUploadController.tsx +162 -62
- package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
- package/dist/components/FireCMSAppBar.d.ts +0 -26
- 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/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/EntityCollectionTable/internal/popup_field/ElementResizeListener.tsx +0 -59
- 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/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,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import equal from "react-fast-compare"
|
|
3
|
+
import { useBlocker, useNavigate } from "react-router-dom";
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
AuthController,
|
|
@@ -9,10 +10,13 @@ import {
|
|
|
9
10
|
EntityCollection,
|
|
10
11
|
EntityCollectionsBuilder,
|
|
11
12
|
EntityReference,
|
|
13
|
+
FireCMSPlugin,
|
|
14
|
+
NavigationBlocker,
|
|
12
15
|
NavigationController,
|
|
16
|
+
NavigationEntry,
|
|
17
|
+
NavigationGroupMapping,
|
|
18
|
+
NavigationResult,
|
|
13
19
|
PermissionsBuilder,
|
|
14
|
-
TopNavigationEntry,
|
|
15
|
-
TopNavigationResult,
|
|
16
20
|
User,
|
|
17
21
|
UserConfigurationPersistence
|
|
18
22
|
} from "../types";
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
applyPermissionsFunctionIfEmpty,
|
|
21
25
|
getCollectionByPathOrId,
|
|
22
26
|
mergeDeep,
|
|
27
|
+
removeFunctions,
|
|
23
28
|
removeInitialAndTrailingSlashes,
|
|
24
29
|
resolveCollectionPathIds,
|
|
25
30
|
resolvePermissions
|
|
@@ -29,28 +34,76 @@ import { getParentReferencesFromPath } from "../util/parent_references_from_path
|
|
|
29
34
|
const DEFAULT_BASE_PATH = "/";
|
|
30
35
|
const DEFAULT_COLLECTION_PATH = "/c";
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
|
|
38
|
+
export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
|
|
39
|
+
|
|
40
|
+
export type BuildNavigationContextProps<EC extends EntityCollection, USER extends User> = {
|
|
41
|
+
/**
|
|
42
|
+
* Base path for the CMS, used to build the all the URLs.
|
|
43
|
+
* Defaults to "/".
|
|
44
|
+
*/
|
|
33
45
|
basePath?: string,
|
|
46
|
+
/**
|
|
47
|
+
* Base path for the collections, used to build the collection URLs.
|
|
48
|
+
* Defaults to "c" (e.g. "/c/products").
|
|
49
|
+
*/
|
|
34
50
|
baseCollectionPath?: string,
|
|
35
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The auth controller used to manage the user authentication and permissions.
|
|
53
|
+
*/
|
|
54
|
+
authController: AuthController<USER>;
|
|
55
|
+
/**
|
|
56
|
+
* The collections to be used in the CMS.
|
|
57
|
+
* This can be a static array of collections or a function that returns a promise
|
|
58
|
+
* resolving to an array of collections.
|
|
59
|
+
*/
|
|
36
60
|
collections?: EC[] | EntityCollectionsBuilder<EC>;
|
|
61
|
+
/**
|
|
62
|
+
* Optional permissions builder to be applied to the collections.
|
|
63
|
+
* If not provided, the permissions will be resolved from the collection configuration.
|
|
64
|
+
*/
|
|
37
65
|
collectionPermissions?: PermissionsBuilder;
|
|
66
|
+
/**
|
|
67
|
+
* Custom views to be added to the CMS, these will be available in the main navigation.
|
|
68
|
+
* This can be a static array of views or a function that returns a promise
|
|
69
|
+
* resolving to an array of views.
|
|
70
|
+
*/
|
|
38
71
|
views?: CMSView[] | CMSViewsBuilder;
|
|
72
|
+
/**
|
|
73
|
+
* Custom views to be added to the CMS admin navigation.
|
|
74
|
+
* This can be a static array of views or a function that returns a promise
|
|
75
|
+
* resolving to an array of views.
|
|
76
|
+
*/
|
|
39
77
|
adminViews?: CMSView[] | CMSViewsBuilder;
|
|
40
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Controller for storing user preferences.
|
|
80
|
+
*/
|
|
41
81
|
userConfigPersistence?: UserConfigurationPersistence;
|
|
82
|
+
/**
|
|
83
|
+
* Delegate for data source operations, used to resolve collections and views.
|
|
84
|
+
*/
|
|
42
85
|
dataSourceDelegate: DataSourceDelegate;
|
|
43
86
|
/**
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
87
|
+
* Plugins to be used in the CMS.
|
|
88
|
+
*/
|
|
89
|
+
plugins?: FireCMSPlugin[];
|
|
90
|
+
/**
|
|
91
|
+
* Used to define the name of groups and order of the navigation entries.
|
|
92
|
+
*/
|
|
93
|
+
navigationGroupMappings?: NavigationGroupMapping[];
|
|
94
|
+
/**
|
|
95
|
+
* If true, the navigation logic will not be updated until this flag is false
|
|
96
|
+
*/
|
|
97
|
+
disabled?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated
|
|
101
|
+
* Use `navigationGroupMappings` instead.
|
|
49
102
|
*/
|
|
50
|
-
|
|
103
|
+
viewsOrder?: string[];
|
|
51
104
|
};
|
|
52
105
|
|
|
53
|
-
export function useBuildNavigationController<EC extends EntityCollection,
|
|
106
|
+
export function useBuildNavigationController<EC extends EntityCollection, USER extends User>(props: BuildNavigationContextProps<EC, USER>): NavigationController {
|
|
54
107
|
const {
|
|
55
108
|
basePath = DEFAULT_BASE_PATH,
|
|
56
109
|
baseCollectionPath = DEFAULT_COLLECTION_PATH,
|
|
@@ -60,18 +113,23 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
60
113
|
views: viewsProp,
|
|
61
114
|
adminViews: adminViewsProp,
|
|
62
115
|
viewsOrder,
|
|
116
|
+
plugins,
|
|
63
117
|
userConfigPersistence,
|
|
64
118
|
dataSourceDelegate,
|
|
65
|
-
|
|
119
|
+
disabled,
|
|
120
|
+
navigationGroupMappings
|
|
66
121
|
} = props;
|
|
67
122
|
|
|
123
|
+
const navigate = useNavigate();
|
|
124
|
+
|
|
68
125
|
const collectionsRef = useRef<EntityCollection[] | undefined>();
|
|
69
126
|
const viewsRef = useRef<CMSView[] | undefined>();
|
|
70
127
|
const adminViewsRef = useRef<CMSView[] | undefined>();
|
|
128
|
+
const navigationEntriesOrderRef = useRef<string[] | undefined>();
|
|
71
129
|
|
|
72
130
|
const [initialised, setInitialised] = useState<boolean>(false);
|
|
73
131
|
|
|
74
|
-
const [topLevelNavigation, setTopLevelNavigation] = useState<
|
|
132
|
+
const [topLevelNavigation, setTopLevelNavigation] = useState<NavigationResult | undefined>(undefined);
|
|
75
133
|
const [navigationLoading, setNavigationLoading] = useState<boolean>(true);
|
|
76
134
|
const [navigationLoadingError, setNavigationLoadingError] = useState<Error | undefined>(undefined);
|
|
77
135
|
|
|
@@ -88,118 +146,246 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
88
146
|
const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
|
|
89
147
|
[baseCollectionPath]);
|
|
90
148
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
const allPluginGroups = plugins?.flatMap(plugin => plugin.homePage?.navigationEntries ? plugin.homePage.navigationEntries.map(e => e.name) : []) ?? [];
|
|
150
|
+
const pluginGroups = [...new Set(allPluginGroups)];
|
|
151
|
+
|
|
152
|
+
const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[], navigationGroupMappingsOverride?: NavigationGroupMapping[], onNavigationEntriesUpdateCallback?: (entries: NavigationGroupMapping[]) => void): NavigationResult => {
|
|
153
|
+
|
|
154
|
+
const finalNavigationGroupMappings: NavigationGroupMapping[] = computeNavigationGroups({
|
|
155
|
+
navigationGroupMappings: navigationGroupMappingsOverride ?? navigationGroupMappings,
|
|
156
|
+
collections,
|
|
157
|
+
views,
|
|
158
|
+
plugins: plugins
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const allPluginNavigationEntries = finalNavigationGroupMappings.map((g) => g.entries).flat() ?? [];
|
|
162
|
+
const navigationEntriesOrder = ([...new Set(allPluginNavigationEntries)]);
|
|
163
|
+
|
|
164
|
+
let navigationEntries: NavigationEntry[] = [
|
|
165
|
+
...(collections ?? []).reduce((acc, collection) => {
|
|
166
|
+
if (collection.hideFromNavigation) return acc;
|
|
167
|
+
|
|
168
|
+
const pathKey = collection.id ?? collection.path;
|
|
169
|
+
let groupName = getGroup(collection); // Initial group
|
|
170
|
+
|
|
171
|
+
if (finalNavigationGroupMappings) {
|
|
172
|
+
for (const pluginGroupDef of finalNavigationGroupMappings) {
|
|
173
|
+
if (pluginGroupDef.entries.includes(pathKey)) {
|
|
174
|
+
groupName = pluginGroupDef.name;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
acc.push({
|
|
181
|
+
id: `collection:${pathKey}`,
|
|
182
|
+
url: buildUrlCollectionPath(pathKey),
|
|
96
183
|
type: "collection",
|
|
97
184
|
name: collection.name.trim(),
|
|
98
|
-
path:
|
|
185
|
+
path: pathKey,
|
|
99
186
|
collection,
|
|
100
187
|
description: collection.description?.trim(),
|
|
101
|
-
group:
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
:
|
|
130
|
-
|
|
188
|
+
group: groupName ?? NAVIGATION_DEFAULT_GROUP_NAME
|
|
189
|
+
});
|
|
190
|
+
return acc;
|
|
191
|
+
}, [] as NavigationEntry[]),
|
|
192
|
+
|
|
193
|
+
...(views ?? []).reduce((acc, view) => {
|
|
194
|
+
if (view.hideFromNavigation) return acc;
|
|
195
|
+
|
|
196
|
+
const pathKey = view.path;
|
|
197
|
+
let groupName = getGroup(view); // Initial group
|
|
198
|
+
|
|
199
|
+
if (finalNavigationGroupMappings) {
|
|
200
|
+
for (const pluginGroupDef of finalNavigationGroupMappings) {
|
|
201
|
+
if (pluginGroupDef.entries.includes(pathKey)) {
|
|
202
|
+
groupName = pluginGroupDef.name;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
acc.push({
|
|
209
|
+
id: `view:${pathKey}`,
|
|
210
|
+
url: buildCMSUrlPath(pathKey),
|
|
211
|
+
name: view.name.trim(),
|
|
212
|
+
type: "view",
|
|
213
|
+
path: view.path,
|
|
214
|
+
view,
|
|
215
|
+
description: view.description?.trim(),
|
|
216
|
+
group: groupName ?? NAVIGATION_DEFAULT_GROUP_NAME
|
|
217
|
+
});
|
|
218
|
+
return acc;
|
|
219
|
+
}, [] as NavigationEntry[]),
|
|
220
|
+
|
|
221
|
+
...(adminViews ?? []).reduce((acc, view) => {
|
|
222
|
+
if (view.hideFromNavigation) return acc;
|
|
223
|
+
|
|
224
|
+
const pathKey = view.path;
|
|
225
|
+
const groupName = NAVIGATION_ADMIN_GROUP_NAME;
|
|
226
|
+
|
|
227
|
+
acc.push({
|
|
228
|
+
id: `admin:${pathKey}`,
|
|
229
|
+
url: buildCMSUrlPath(pathKey),
|
|
230
|
+
name: view.name.trim(),
|
|
231
|
+
type: "admin",
|
|
232
|
+
path: view.path,
|
|
233
|
+
view,
|
|
234
|
+
description: view.description?.trim(),
|
|
235
|
+
group: groupName
|
|
236
|
+
});
|
|
237
|
+
return acc;
|
|
238
|
+
}, [] as NavigationEntry[])
|
|
131
239
|
];
|
|
132
240
|
|
|
133
|
-
|
|
241
|
+
const groupOrderValue = (groupName?: string): number => {
|
|
242
|
+
if (groupName === NAVIGATION_ADMIN_GROUP_NAME) return 1;
|
|
243
|
+
return 0; // Other groups
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
navigationEntries = navigationEntries.sort((a, b) => {
|
|
247
|
+
return groupOrderValue(a.group) - groupOrderValue(b.group);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
|
|
251
|
+
if (usedViewsOrder) {
|
|
134
252
|
navigationEntries = navigationEntries.sort((a, b) => {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
return 1;
|
|
142
|
-
}
|
|
143
|
-
if (bIndex === -1) {
|
|
144
|
-
return -1;
|
|
145
|
-
}
|
|
253
|
+
const getSortPath = (navEntry: NavigationEntry) => typeof navEntry.path === "string" ? navEntry.path : navEntry.path[0];
|
|
254
|
+
const aIndex = usedViewsOrder.indexOf(getSortPath(a));
|
|
255
|
+
const bIndex = usedViewsOrder.indexOf(getSortPath(b));
|
|
256
|
+
if (aIndex === -1 && bIndex === -1) return 0;
|
|
257
|
+
if (aIndex === -1) return 1;
|
|
258
|
+
if (bIndex === -1) return -1;
|
|
146
259
|
return aIndex - bIndex;
|
|
147
260
|
});
|
|
148
261
|
}
|
|
149
262
|
|
|
150
|
-
const
|
|
263
|
+
const collectedGroupsFromEntries = navigationEntries
|
|
151
264
|
.map(e => e.group)
|
|
152
|
-
.filter(Boolean)
|
|
153
|
-
|
|
265
|
+
.filter(Boolean) as string[];
|
|
266
|
+
|
|
267
|
+
// Preserve order from finalNavigationGroupMappings (persisted order)
|
|
268
|
+
const groupsFromMappings = finalNavigationGroupMappings.map(g => g.name);
|
|
269
|
+
|
|
270
|
+
// Add any additional groups not in mappings
|
|
271
|
+
const additionalGroups = collectedGroupsFromEntries.filter(g => !groupsFromMappings.includes(g));
|
|
272
|
+
|
|
273
|
+
const allDefinedGroups = [
|
|
274
|
+
...(pluginGroups ?? []),
|
|
275
|
+
...groupsFromMappings,
|
|
276
|
+
...additionalGroups
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// Remove duplicates while preserving order, then separate admin to the end
|
|
280
|
+
const uniqueGroupsArray = [...new Set(allDefinedGroups)];
|
|
281
|
+
const adminGroups = uniqueGroupsArray.filter(g => g === NAVIGATION_ADMIN_GROUP_NAME);
|
|
282
|
+
const nonAdminGroups = uniqueGroupsArray.filter(g => g !== NAVIGATION_ADMIN_GROUP_NAME);
|
|
283
|
+
const uniqueGroups = [...nonAdminGroups, ...adminGroups];
|
|
154
284
|
|
|
155
285
|
return {
|
|
286
|
+
allowDragAndDrop: plugins?.some(plugin => plugin.homePage?.allowDragAndDrop) ?? false,
|
|
156
287
|
navigationEntries,
|
|
157
|
-
groups
|
|
288
|
+
groups: uniqueGroups,
|
|
289
|
+
onNavigationEntriesUpdate: onNavigationEntriesUpdateCallback!,
|
|
158
290
|
};
|
|
159
|
-
}, [buildCMSUrlPath, buildUrlCollectionPath]);
|
|
291
|
+
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups]);
|
|
292
|
+
|
|
293
|
+
const onNavigationEntriesOrderUpdate = useCallback((entries: NavigationGroupMapping[]) => {
|
|
294
|
+
if (!plugins) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// remove all groups that have no entries
|
|
298
|
+
const filteredEntries = entries.filter(entry => entry.entries.length > 0);
|
|
299
|
+
|
|
300
|
+
// Immediately update the local topLevelNavigation with new mappings
|
|
301
|
+
if (collectionsRef.current && viewsRef.current) {
|
|
302
|
+
const updatedNav = computeTopNavigation(
|
|
303
|
+
collectionsRef.current,
|
|
304
|
+
viewsRef.current,
|
|
305
|
+
adminViewsRef.current ?? [],
|
|
306
|
+
viewsOrder,
|
|
307
|
+
filteredEntries,
|
|
308
|
+
onNavigationEntriesOrderUpdate
|
|
309
|
+
);
|
|
310
|
+
setTopLevelNavigation(updatedNav);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Then persist to backend
|
|
314
|
+
if (plugins.some(plugin => plugin.homePage?.onNavigationEntriesUpdate)) {
|
|
315
|
+
plugins.forEach(plugin => {
|
|
316
|
+
if (plugin.homePage?.onNavigationEntriesUpdate) {
|
|
317
|
+
plugin.homePage.onNavigationEntriesUpdate(filteredEntries);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
}, [plugins, computeTopNavigation, viewsOrder]);
|
|
160
323
|
|
|
161
324
|
const refreshNavigation = useCallback(async () => {
|
|
162
325
|
|
|
163
|
-
if (authController.initialLoading)
|
|
326
|
+
if (disabled || authController.initialLoading)
|
|
164
327
|
return;
|
|
165
328
|
|
|
329
|
+
console.debug("Refreshing navigation");
|
|
330
|
+
|
|
166
331
|
try {
|
|
167
332
|
|
|
168
333
|
const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
|
|
169
|
-
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate,
|
|
334
|
+
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, plugins),
|
|
170
335
|
resolveCMSViews(viewsProp, authController, dataSourceDelegate),
|
|
171
336
|
resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
|
|
172
337
|
]
|
|
173
338
|
);
|
|
174
339
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
340
|
+
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder, undefined, onNavigationEntriesOrderUpdate);
|
|
341
|
+
|
|
342
|
+
let shouldUpdateTopLevelNav = false;
|
|
343
|
+
if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
344
|
+
collectionsRef.current = resolvedCollections;
|
|
345
|
+
console.debug("Collections have changed", resolvedCollections);
|
|
346
|
+
shouldUpdateTopLevelNav = true;
|
|
347
|
+
}
|
|
348
|
+
if (collectionsRef.current === undefined) {
|
|
181
349
|
collectionsRef.current = resolvedCollections;
|
|
350
|
+
shouldUpdateTopLevelNav = true;
|
|
351
|
+
}
|
|
352
|
+
if (!equal(viewsRef.current, resolvedViews)) {
|
|
182
353
|
viewsRef.current = resolvedViews;
|
|
354
|
+
shouldUpdateTopLevelNav = true;
|
|
355
|
+
}
|
|
356
|
+
if (!equal(adminViewsRef.current, resolvedAdminViews)) {
|
|
183
357
|
adminViewsRef.current = resolvedAdminViews;
|
|
184
|
-
|
|
358
|
+
shouldUpdateTopLevelNav = true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const navigationEntriesOrder = computedTopLevelNav.navigationEntries.map(e => e.id);
|
|
362
|
+
if (!equal(navigationEntriesOrderRef.current, navigationEntriesOrder)) {
|
|
363
|
+
navigationEntriesOrderRef.current = navigationEntriesOrder;
|
|
364
|
+
shouldUpdateTopLevelNav = true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (shouldUpdateTopLevelNav && !equal(topLevelNavigation, computedTopLevelNav)) {
|
|
368
|
+
setTopLevelNavigation(computedTopLevelNav);
|
|
185
369
|
}
|
|
186
370
|
} catch (e) {
|
|
187
371
|
console.error(e);
|
|
188
372
|
setNavigationLoadingError(e as any);
|
|
189
373
|
}
|
|
190
374
|
|
|
191
|
-
|
|
192
|
-
|
|
375
|
+
if (navigationLoading)
|
|
376
|
+
setNavigationLoading(false);
|
|
377
|
+
if (!initialised)
|
|
378
|
+
setInitialised(true);
|
|
193
379
|
|
|
194
380
|
}, [
|
|
195
381
|
collectionsProp,
|
|
196
382
|
collectionPermissions,
|
|
197
383
|
authController.user,
|
|
198
384
|
authController.initialLoading,
|
|
385
|
+
disabled,
|
|
199
386
|
viewsProp,
|
|
200
387
|
adminViewsProp,
|
|
201
388
|
computeTopNavigation,
|
|
202
|
-
injectCollections
|
|
203
389
|
]);
|
|
204
390
|
|
|
205
391
|
useEffect(() => {
|
|
@@ -208,7 +394,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
208
394
|
|
|
209
395
|
const getCollection = useCallback((
|
|
210
396
|
idOrPath: string,
|
|
211
|
-
entityId?: string,
|
|
212
397
|
includeUserOverride = false
|
|
213
398
|
): EC | undefined => {
|
|
214
399
|
const collections = collectionsRef.current;
|
|
@@ -218,8 +403,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
218
403
|
const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
|
|
219
404
|
|
|
220
405
|
const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
|
|
221
|
-
|
|
222
|
-
const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride) : undefined;
|
|
406
|
+
const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride ?? {}) : undefined;
|
|
223
407
|
|
|
224
408
|
let result: Partial<EntityCollection> | undefined = overriddenCollection;
|
|
225
409
|
|
|
@@ -241,12 +425,22 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
241
425
|
|
|
242
426
|
}, [userConfigPersistence]);
|
|
243
427
|
|
|
428
|
+
const getCollectionById = useCallback((id: string): EC | undefined => {
|
|
429
|
+
const collections = collectionsRef.current;
|
|
430
|
+
if (collections === undefined)
|
|
431
|
+
throw Error("getCollectionById: Collections have not been initialised yet");
|
|
432
|
+
const collection: EntityCollection | undefined = collections.find(c => c.id === id);
|
|
433
|
+
if (!collection)
|
|
434
|
+
return undefined;
|
|
435
|
+
return collection as EC;
|
|
436
|
+
}, []);
|
|
437
|
+
|
|
244
438
|
const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
|
|
245
439
|
|
|
246
440
|
const collections = collectionsRef.current;
|
|
441
|
+
if (collections === undefined)
|
|
442
|
+
throw Error("getCollectionFromPaths: Collections have not been initialised yet");
|
|
247
443
|
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
248
|
-
if (!currentCollections)
|
|
249
|
-
throw Error("Collections have not been initialised yet");
|
|
250
444
|
|
|
251
445
|
for (let i = 0; i < pathSegments.length; i++) {
|
|
252
446
|
const pathSegment = pathSegments[i];
|
|
@@ -265,9 +459,9 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
265
459
|
const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
|
|
266
460
|
|
|
267
461
|
const collections = collectionsRef.current;
|
|
462
|
+
if (collections === undefined)
|
|
463
|
+
throw Error("getCollectionFromIds: Collections have not been initialised yet");
|
|
268
464
|
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
269
|
-
if (!currentCollections)
|
|
270
|
-
throw Error("Collections have not been initialised yet");
|
|
271
465
|
|
|
272
466
|
for (let i = 0; i < ids.length; i++) {
|
|
273
467
|
const id = ids[i];
|
|
@@ -288,24 +482,14 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
288
482
|
[fullCollectionPath]);
|
|
289
483
|
|
|
290
484
|
const urlPathToDataPath = useCallback((path: string): string => {
|
|
291
|
-
|
|
292
|
-
|
|
485
|
+
const decodedPath = decodeURIComponent(path);
|
|
486
|
+
if (decodedPath.startsWith(fullCollectionPath))
|
|
487
|
+
return decodedPath.replace(fullCollectionPath, "");
|
|
293
488
|
throw Error("Expected path starting with " + fullCollectionPath);
|
|
294
489
|
}, [fullCollectionPath]);
|
|
295
490
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
}: {
|
|
299
|
-
path: string
|
|
300
|
-
}): string => {
|
|
301
|
-
return `s/edit/${encodePath(path)}`;
|
|
302
|
-
},
|
|
303
|
-
[]);
|
|
304
|
-
|
|
305
|
-
const resolveAliasesFrom = useCallback((path: string): string => {
|
|
306
|
-
const collections = collectionsRef.current;
|
|
307
|
-
if (!collections)
|
|
308
|
-
throw Error("Collections have not been initialised yet");
|
|
491
|
+
const resolveIdsFrom = useCallback((path: string): string => {
|
|
492
|
+
const collections = collectionsRef.current ?? [];
|
|
309
493
|
return resolveCollectionPathIds(path, collections);
|
|
310
494
|
}, []);
|
|
311
495
|
|
|
@@ -359,29 +543,23 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
359
543
|
baseCollectionPath,
|
|
360
544
|
initialised,
|
|
361
545
|
getCollection,
|
|
546
|
+
getCollectionById,
|
|
362
547
|
getCollectionFromPaths,
|
|
363
548
|
getCollectionFromIds,
|
|
364
549
|
isUrlCollectionPath,
|
|
365
550
|
urlPathToDataPath,
|
|
366
551
|
buildUrlCollectionPath,
|
|
367
|
-
|
|
368
|
-
buildCMSUrlPath,
|
|
369
|
-
resolveAliasesFrom,
|
|
552
|
+
resolveIdsFrom,
|
|
370
553
|
topLevelNavigation,
|
|
371
554
|
refreshNavigation,
|
|
372
555
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
373
556
|
getParentCollectionIds,
|
|
374
|
-
convertIdsToPaths
|
|
557
|
+
convertIdsToPaths,
|
|
558
|
+
navigate,
|
|
559
|
+
plugins
|
|
375
560
|
};
|
|
376
561
|
}
|
|
377
562
|
|
|
378
|
-
export function getSidePanelKey(path: string, entityId?: string) {
|
|
379
|
-
if (entityId)
|
|
380
|
-
return `${removeInitialAndTrailingSlashes(path)}/${removeInitialAndTrailingSlashes(entityId)}`;
|
|
381
|
-
else
|
|
382
|
-
return removeInitialAndTrailingSlashes(path);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
563
|
function encodePath(input: string) {
|
|
386
564
|
return encodeURIComponent(removeInitialAndTrailingSlashes(input))
|
|
387
565
|
.replaceAll("%2F", "/")
|
|
@@ -390,9 +568,10 @@ function encodePath(input: string) {
|
|
|
390
568
|
|
|
391
569
|
function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[], authController: AuthController<User>): EntityCollection[] {
|
|
392
570
|
return resolvedCollections
|
|
571
|
+
.filter((c) => Boolean(c.path))
|
|
393
572
|
.filter((c) => {
|
|
394
573
|
if (!c.permissions) return true;
|
|
395
|
-
const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
|
|
574
|
+
const resolvedPermissions = resolvePermissions(c, authController, c.path, null);
|
|
396
575
|
return resolvedPermissions?.read !== false;
|
|
397
576
|
})
|
|
398
577
|
.map((c) => {
|
|
@@ -404,11 +583,24 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
404
583
|
});
|
|
405
584
|
}
|
|
406
585
|
|
|
586
|
+
function applyPluginModifyCollection(resolvedCollections: EntityCollection[], modifyCollection: (collection: EntityCollection) => EntityCollection) {
|
|
587
|
+
return resolvedCollections.map((collection: EntityCollection): EntityCollection => {
|
|
588
|
+
const modifiedCollection = modifyCollection(collection);
|
|
589
|
+
if (modifiedCollection.subcollections) {
|
|
590
|
+
return {
|
|
591
|
+
...modifiedCollection,
|
|
592
|
+
subcollections: applyPluginModifyCollection(modifiedCollection.subcollections, modifyCollection)
|
|
593
|
+
} satisfies EntityCollection;
|
|
594
|
+
}
|
|
595
|
+
return modifiedCollection;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
407
599
|
async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
|
|
408
600
|
collectionPermissions: PermissionsBuilder | undefined,
|
|
409
601
|
authController: AuthController,
|
|
410
602
|
dataSource: DataSourceDelegate,
|
|
411
|
-
|
|
603
|
+
plugins: FireCMSPlugin[] | undefined): Promise<EntityCollection[]> {
|
|
412
604
|
let resolvedCollections: EntityCollection[] = [];
|
|
413
605
|
if (typeof collections === "function") {
|
|
414
606
|
resolvedCollections = await collections({
|
|
@@ -420,14 +612,20 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
|
|
|
420
612
|
resolvedCollections = collections;
|
|
421
613
|
}
|
|
422
614
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
615
|
+
if (plugins) {
|
|
616
|
+
for (const plugin of plugins) {
|
|
617
|
+
if (plugin.collection?.modifyCollection) {
|
|
618
|
+
resolvedCollections = applyPluginModifyCollection(resolvedCollections, plugin.collection.modifyCollection);
|
|
619
|
+
}
|
|
426
620
|
|
|
427
|
-
|
|
428
|
-
|
|
621
|
+
if (plugin.collection?.injectCollections) {
|
|
622
|
+
resolvedCollections = plugin.collection.injectCollections(resolvedCollections ?? []);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
429
625
|
}
|
|
430
626
|
|
|
627
|
+
resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
|
|
628
|
+
resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
|
|
431
629
|
return resolvedCollections;
|
|
432
630
|
}
|
|
433
631
|
|
|
@@ -448,7 +646,203 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
448
646
|
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
449
647
|
const trimmed = collectionOrView.group?.trim();
|
|
450
648
|
if (!trimmed || trimmed === "") {
|
|
451
|
-
return
|
|
649
|
+
return NAVIGATION_DEFAULT_GROUP_NAME;
|
|
650
|
+
}
|
|
651
|
+
return trimmed ?? NAVIGATION_DEFAULT_GROUP_NAME;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
655
|
+
if (a.length !== b.length) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
const aCopy = [...a];
|
|
659
|
+
const bCopy = [...b];
|
|
660
|
+
const aSorted = aCopy.sort((x, y) => x.id.localeCompare(y.id));
|
|
661
|
+
const bSorted = bCopy.sort((x, y) => x.id.localeCompare(y.id));
|
|
662
|
+
return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function areCollectionsEqual(a: EntityCollection, b: EntityCollection) {
|
|
666
|
+
const {
|
|
667
|
+
subcollections: subcollectionsA,
|
|
668
|
+
...restA
|
|
669
|
+
} = a;
|
|
670
|
+
const {
|
|
671
|
+
subcollections: subcollectionsB,
|
|
672
|
+
...restB
|
|
673
|
+
} = b;
|
|
674
|
+
if (!areCollectionListsEqual(subcollectionsA ?? [], subcollectionsB ?? [])) {
|
|
675
|
+
return false;
|
|
452
676
|
}
|
|
453
|
-
return
|
|
677
|
+
return equal(removeFunctions(restA), removeFunctions(restB));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function useCustomBlocker(): NavigationBlocker {
|
|
681
|
+
const [blockListeners, setBlockListeners] = useState<Record<string, {
|
|
682
|
+
block: boolean,
|
|
683
|
+
basePath?: string
|
|
684
|
+
}>>({});
|
|
685
|
+
|
|
686
|
+
const shouldBlock = Object.values(blockListeners).some(b => b.block);
|
|
687
|
+
|
|
688
|
+
let blocker: any;
|
|
689
|
+
try {
|
|
690
|
+
blocker = useBlocker(({
|
|
691
|
+
nextLocation
|
|
692
|
+
}) => {
|
|
693
|
+
const allBasePaths = Object.values(blockListeners).map(b => b.basePath).filter(Boolean) as string[];
|
|
694
|
+
if (allBasePaths && allBasePaths.some(path => nextLocation.pathname.startsWith(path)))
|
|
695
|
+
return false;
|
|
696
|
+
return shouldBlock;
|
|
697
|
+
});
|
|
698
|
+
} catch (e) {
|
|
699
|
+
// console.warn("Blocker not available, navigation will not be blocked");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const updateBlockListener = (path: string, block: boolean, basePath?: string) => {
|
|
703
|
+
setBlockListeners(prev => ({
|
|
704
|
+
...prev,
|
|
705
|
+
[path]: {
|
|
706
|
+
block,
|
|
707
|
+
basePath
|
|
708
|
+
}
|
|
709
|
+
}));
|
|
710
|
+
return () => setBlockListeners(prev => {
|
|
711
|
+
const {
|
|
712
|
+
[path]: removed,
|
|
713
|
+
...rest
|
|
714
|
+
} = prev;
|
|
715
|
+
return rest;
|
|
716
|
+
})
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const isBlocked = (path: string) => {
|
|
720
|
+
return (blockListeners[path]?.block ?? false) && blocker?.state === "blocked";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
updateBlockListener,
|
|
725
|
+
isBlocked,
|
|
726
|
+
proceed: blocker?.proceed,
|
|
727
|
+
reset: blocker?.reset
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function computeNavigationGroups({
|
|
732
|
+
navigationGroupMappings,
|
|
733
|
+
collections,
|
|
734
|
+
views,
|
|
735
|
+
plugins
|
|
736
|
+
}: {
|
|
737
|
+
navigationGroupMappings?: NavigationGroupMapping[],
|
|
738
|
+
collections?: EntityCollection[],
|
|
739
|
+
views?: CMSView[],
|
|
740
|
+
plugins?: FireCMSPlugin[]
|
|
741
|
+
}): NavigationGroupMapping[] {
|
|
742
|
+
|
|
743
|
+
let result = navigationGroupMappings;
|
|
744
|
+
|
|
745
|
+
// Merge plugin navigation entries
|
|
746
|
+
result = plugins ? plugins?.reduce((acc, plugin) => {
|
|
747
|
+
if (plugin.homePage?.navigationEntries) {
|
|
748
|
+
plugin.homePage.navigationEntries.forEach((entry) => {
|
|
749
|
+
const {
|
|
750
|
+
name,
|
|
751
|
+
entries
|
|
752
|
+
} = entry;
|
|
753
|
+
const existingGroup = acc.find(entry => entry.name === name);
|
|
754
|
+
if (existingGroup) {
|
|
755
|
+
existingGroup.entries.push(...entries);
|
|
756
|
+
} else {
|
|
757
|
+
acc.push({
|
|
758
|
+
name,
|
|
759
|
+
entries: [...entries]
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
}
|
|
765
|
+
return acc;
|
|
766
|
+
}, [...(result ?? [])] as NavigationGroupMapping[]) : result;
|
|
767
|
+
|
|
768
|
+
// Track all entries that are already assigned to groups
|
|
769
|
+
const assignedEntries = new Set<string>();
|
|
770
|
+
if (result) {
|
|
771
|
+
result.forEach(group => {
|
|
772
|
+
group.entries.forEach(entry => assignedEntries.add(entry));
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Find collections and views that are NOT in any persisted group
|
|
777
|
+
const unassignedGroupMap: Record<string, string[]> = {};
|
|
778
|
+
|
|
779
|
+
// Check collections
|
|
780
|
+
(collections ?? []).forEach(collection => {
|
|
781
|
+
const entry = collection.id ?? collection.path;
|
|
782
|
+
if (!assignedEntries.has(entry)) {
|
|
783
|
+
const groupName = getGroup(collection);
|
|
784
|
+
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
785
|
+
unassignedGroupMap[groupName].push(entry);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Check views
|
|
790
|
+
(views ?? []).forEach(view => {
|
|
791
|
+
const entry = view.path;
|
|
792
|
+
if (!assignedEntries.has(entry)) {
|
|
793
|
+
const groupName = getGroup(view);
|
|
794
|
+
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
795
|
+
unassignedGroupMap[groupName].push(entry);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Merge unassigned entries into existing groups or create new groups
|
|
800
|
+
Object.entries(unassignedGroupMap).forEach(([groupName, entries]) => {
|
|
801
|
+
if (result) {
|
|
802
|
+
const existingGroup = result.find(g => g.name === groupName);
|
|
803
|
+
if (existingGroup) {
|
|
804
|
+
existingGroup.entries.push(...entries);
|
|
805
|
+
} else {
|
|
806
|
+
result.push({
|
|
807
|
+
name: groupName,
|
|
808
|
+
entries
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (!result) {
|
|
815
|
+
// No persisted data at all - create from scratch
|
|
816
|
+
result = [];
|
|
817
|
+
const groupMap: Record<string, string[]> = {};
|
|
818
|
+
|
|
819
|
+
// Add collections
|
|
820
|
+
(collections ?? []).forEach(collection => {
|
|
821
|
+
const name = getGroup(collection);
|
|
822
|
+
const entry = collection.id ?? collection.path;
|
|
823
|
+
if (!groupMap[name]) groupMap[name] = [];
|
|
824
|
+
groupMap[name].push(entry);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// Add views
|
|
828
|
+
(views ?? []).forEach(view => {
|
|
829
|
+
const name = getGroup(view);
|
|
830
|
+
const entry = view.path;
|
|
831
|
+
if (!groupMap[name]) groupMap[name] = [];
|
|
832
|
+
groupMap[name].push(entry);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Convert groupMap to result array
|
|
836
|
+
result = Object.entries(groupMap).map(([name, entries]) => ({
|
|
837
|
+
name,
|
|
838
|
+
entries
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Remove duplicates in entries
|
|
843
|
+
result.forEach(group => {
|
|
844
|
+
group.entries = [...new Set(group.entries)];
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
return result;
|
|
454
848
|
}
|