From 20c9a7691522d4fef67f02a14696dd8fa2e8a363 Mon Sep 17 00:00:00 2001 From: AselPeiris Date: Fri, 17 Jan 2025 17:35:58 +0530 Subject: [PATCH] fix: extracting menu item filtering to a hook --- frontend/src/hooks/useAvailableMenuItems.ts | 72 +++++++++++++++++++++ frontend/src/hooks/useHasPermission.ts | 5 +- frontend/src/layout/VerticalMenu.tsx | 43 +----------- 3 files changed, 78 insertions(+), 42 deletions(-) create mode 100644 frontend/src/hooks/useAvailableMenuItems.ts diff --git a/frontend/src/hooks/useAvailableMenuItems.ts b/frontend/src/hooks/useAvailableMenuItems.ts new file mode 100644 index 00000000..ca3a54c2 --- /dev/null +++ b/frontend/src/hooks/useAvailableMenuItems.ts @@ -0,0 +1,72 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { useMemo } from "react"; + +import { MenuItem } from "@/layout/VerticalMenu"; +import { EntityType } from "@/services/types"; +import { PermissionAction } from "@/types/permission.types"; + +import { useHasPermission } from "./useHasPermission"; + +// Helper function to check permissions for a menu item +const isMenuItemAllowed = ( + menuItem: MenuItem, + hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, +): boolean => { + const requiredPermissions = Object.entries(menuItem.requires || {}); + + return ( + requiredPermissions.length === 0 || + requiredPermissions.every(([entityType, actions]) => + actions.every((action) => + hasPermission(entityType as EntityType, action), + ), + ) + ); +}; +const filterMenuItems = ( + menuItems: MenuItem[], + hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, +): MenuItem[] => { + return menuItems + .map((menuItem) => { + // Validate top-level menu item without submenu + if ( + menuItem && + !menuItem.submenuItems && + isMenuItemAllowed(menuItem, hasPermission) + ) { + return menuItem; + } + + // Recursively process submenu items + if (menuItem.submenuItems) { + const filteredSubmenuItems = filterMenuItems( + menuItem.submenuItems, + hasPermission, + ); + + if (filteredSubmenuItems.length > 0) { + return { ...menuItem, submenuItems: filteredSubmenuItems }; + } + } + + return null; // Exclude invalid menu items + }) + .filter((menuItem): menuItem is MenuItem => !!menuItem); +}; +const useAvailableMenuItems = (menuItems: MenuItem[]): MenuItem[] => { + const hasPermission = useHasPermission(); + + return useMemo(() => { + return filterMenuItems(menuItems, hasPermission); + }, [menuItems, hasPermission]); +}; + +export default useAvailableMenuItems; diff --git a/frontend/src/hooks/useHasPermission.ts b/frontend/src/hooks/useHasPermission.ts index 09b6be68..1c1fa2a3 100644 --- a/frontend/src/hooks/useHasPermission.ts +++ b/frontend/src/hooks/useHasPermission.ts @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { useCallback, useContext } from "react"; import { PermissionContext } from "@/contexts/permission.context"; @@ -18,7 +19,7 @@ export const useHasPermission = () => { (type: EntityType, action: PermissionAction) => { const allowedActions = getAllowedActions(type); - return allowedActions?.includes(action); + return allowedActions?.includes(action) ? true : false; }, [getAllowedActions], ); diff --git a/frontend/src/layout/VerticalMenu.tsx b/frontend/src/layout/VerticalMenu.tsx index 8112485a..857f9dfa 100644 --- a/frontend/src/layout/VerticalMenu.tsx +++ b/frontend/src/layout/VerticalMenu.tsx @@ -6,7 +6,6 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ - import { faAlignLeft, faAsterisk, @@ -33,13 +32,13 @@ import { CSSObject, Grid, IconButton, styled, Theme } from "@mui/material"; import MuiDrawer from "@mui/material/Drawer"; import { OverridableComponent } from "@mui/material/OverridableComponent"; import { useRouter } from "next/router"; -import { FC, useMemo } from "react"; +import { FC } from "react"; import { HexabotLogo } from "@/app-components/logos/HexabotLogo"; import { Sidebar } from "@/app-components/menus/Sidebar"; import { useAuth } from "@/hooks/useAuth"; +import useAvailableMenuItems from "@/hooks/useAvailableMenuItems"; import { useConfig } from "@/hooks/useConfig"; -import { useHasPermission } from "@/hooks/useHasPermission"; import { EntityType } from "@/services/types"; import { PermissionAction } from "@/types/permission.types"; import { getLayout } from "@/utils/laylout"; @@ -288,44 +287,8 @@ export const VerticalMenu: FC = ({ const { ssoEnabled } = useConfig(); const { isAuthenticated } = useAuth(); const router = useRouter(); - const hasPermission = useHasPermission(); const menuItems = getMenuItems(ssoEnabled); - // Filter menu item to which user is allowed access - const generateValidMenuItems = useMemo(() => { - return (menuItems: MenuItem[]): MenuItem[] => { - const validMenuItems = menuItems - .map((menuItem: MenuItem) => { - if (menuItem && !menuItem.submenuItems) { - const requiredPermissions = menuItem.requires!; - - if ( - requiredPermissions && - Object.entries(requiredPermissions).every((permission) => { - const entityType = permission[0] as EntityType; - const actions = permission[1]; - - return actions.every((action) => - hasPermission(entityType, action), - ); - }) - ) { - return menuItem; - } - } else if (menuItem.submenuItems) { - menuItem.submenuItems = generateValidMenuItems( - menuItem.submenuItems, - ); - - return menuItem; - } - }) - .filter((menuItem) => menuItem !== undefined) - .filter((menuItem) => menuItem?.submenuItems?.length !== 0); - - return validMenuItems; - }; - }, [menuItems, hasPermission]); - const availableMenuItems = generateValidMenuItems(menuItems); + const availableMenuItems = useAvailableMenuItems(menuItems); const hasTemporaryDrawer = getLayout(router.pathname.slice(1)) === "full_width";