From 60ca208846c089d0ed6c68d1c7a2301fc2682a40 Mon Sep 17 00:00:00 2001 From: AselPeiris Date: Sun, 12 Jan 2025 03:00:18 +0530 Subject: [PATCH 1/4] fix(frontend): fix menu accessibility visibility issue --- frontend/src/layout/VerticalMenu.tsx | 50 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/frontend/src/layout/VerticalMenu.tsx b/frontend/src/layout/VerticalMenu.tsx index d826a753..8112485a 100644 --- a/frontend/src/layout/VerticalMenu.tsx +++ b/frontend/src/layout/VerticalMenu.tsx @@ -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 { faAlignLeft, faAsterisk, @@ -39,7 +40,6 @@ import { Sidebar } from "@/app-components/menus/Sidebar"; import { useAuth } from "@/hooks/useAuth"; import { useConfig } from "@/hooks/useConfig"; import { useHasPermission } from "@/hooks/useHasPermission"; -import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; import { PermissionAction } from "@/types/permission.types"; import { getLayout } from "@/utils/laylout"; @@ -206,7 +206,6 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [ }, }, - // { // text: 'menu.broadcast', // href: "/subscribers/broadcast", @@ -287,25 +286,46 @@ export const VerticalMenu: FC = ({ onToggleOut, }) => { const { ssoEnabled } = useConfig(); - const { t } = useTranslate(); const { isAuthenticated } = useAuth(); const router = useRouter(); const hasPermission = useHasPermission(); const menuItems = getMenuItems(ssoEnabled); // Filter menu item to which user is allowed access - const availableMenuItems = useMemo(() => { - return menuItems.filter(({ requires: requiredPermissions }) => { - return ( - !requiredPermissions || - Object.entries(requiredPermissions).every((permission) => { - const entityType = permission[0] as EntityType; - const actions = permission[1]; + const generateValidMenuItems = useMemo(() => { + return (menuItems: MenuItem[]): MenuItem[] => { + const validMenuItems = menuItems + .map((menuItem: MenuItem) => { + if (menuItem && !menuItem.submenuItems) { + const requiredPermissions = menuItem.requires!; - return actions.every((action) => hasPermission(entityType, action)); + 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; + } }) - ); - }); - }, [t, hasPermission]); + .filter((menuItem) => menuItem !== undefined) + .filter((menuItem) => menuItem?.submenuItems?.length !== 0); + + return validMenuItems; + }; + }, [menuItems, hasPermission]); + const availableMenuItems = generateValidMenuItems(menuItems); const hasTemporaryDrawer = getLayout(router.pathname.slice(1)) === "full_width"; From 20c9a7691522d4fef67f02a14696dd8fa2e8a363 Mon Sep 17 00:00:00 2001 From: AselPeiris Date: Fri, 17 Jan 2025 17:35:58 +0530 Subject: [PATCH 2/4] 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"; From ffd4aae3e01b859eb4f3584d4aa17fc181127d30 Mon Sep 17 00:00:00 2001 From: Med Marrouchi Date: Mon, 20 Jan 2025 14:43:11 +0100 Subject: [PATCH 3/4] fix: apply pr review --- frontend/src/hooks/useAvailableMenuItems.ts | 13 ++++++++++++- frontend/src/hooks/useHasPermission.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useAvailableMenuItems.ts b/frontend/src/hooks/useAvailableMenuItems.ts index ca3a54c2..3a702cf7 100644 --- a/frontend/src/hooks/useAvailableMenuItems.ts +++ b/frontend/src/hooks/useAvailableMenuItems.ts @@ -14,7 +14,12 @@ import { PermissionAction } from "@/types/permission.types"; import { useHasPermission } from "./useHasPermission"; -// Helper function to check permissions for a menu item +/** + * Helper function to check permissions for a menu item + * @param menuItem - The menu item + * @param hasPermission - Callback function + * @returns True if hasPermission() is true for all required permissions. + */ const isMenuItemAllowed = ( menuItem: MenuItem, hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, @@ -30,6 +35,12 @@ const isMenuItemAllowed = ( ) ); }; + +/** + * Filters menu items based on user permissions. + * @param menuItems - The list of menu items to filter. + * @returns A filtered list of menu items that the user is allowed to access. + */ const filterMenuItems = ( menuItems: MenuItem[], hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, diff --git a/frontend/src/hooks/useHasPermission.ts b/frontend/src/hooks/useHasPermission.ts index 1c1fa2a3..0d9079c6 100644 --- a/frontend/src/hooks/useHasPermission.ts +++ b/frontend/src/hooks/useHasPermission.ts @@ -19,7 +19,7 @@ export const useHasPermission = () => { (type: EntityType, action: PermissionAction) => { const allowedActions = getAllowedActions(type); - return allowedActions?.includes(action) ? true : false; + return !!allowedActions && allowedActions?.includes(action); }, [getAllowedActions], ); From 60f8429a6356deb7a16b1f41db2c582ddee6d947 Mon Sep 17 00:00:00 2001 From: Med Marrouchi Date: Mon, 20 Jan 2025 14:47:33 +0100 Subject: [PATCH 4/4] fix: lint --- frontend/src/hooks/useAvailableMenuItems.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/hooks/useAvailableMenuItems.ts b/frontend/src/hooks/useAvailableMenuItems.ts index 3a702cf7..f1bfd09d 100644 --- a/frontend/src/hooks/useAvailableMenuItems.ts +++ b/frontend/src/hooks/useAvailableMenuItems.ts @@ -35,7 +35,6 @@ const isMenuItemAllowed = ( ) ); }; - /** * Filters menu items based on user permissions. * @param menuItems - The list of menu items to filter.