From ebe067f65cfb0e26449c569cb81540e4597f920b Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 26 Jun 2026 13:55:09 +0200 Subject: [PATCH] Fix favorite showing non-readable objects (#22217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Navigation menu items backed by objects the user has no read permission on were correctly hidden from the Workspace section, but Favorites still showed them. Favorites are user-scoped nav items (tied to workspaceMemberId), and FavoritesSection only filtered out folder children (!item.folderId) without ever checking canReadObjectRecords. The shared upstream filter (filterAndSortNavigationMenuItems) intentionally does not apply read permissions, because its output also drives drag-and-drop position math and layout-customization/edit mode, which need the complete, unfiltered list. So read-permission filtering belongs at the display layer. ## Fix New shared hook useReadableNavigationMenuItems that centralizes the read-permission filtering logic previously duplicated across sections: wires up objectMetadataItems + views + object permissions around isNavigationMenuItemReadable filters folder children and top-level items (dropping folders whose children are all unreadable) exposes both raw filtered* outputs and isLayoutCustomizationModeEnabled-aware display* outputs FavoritesSection now applies the filter via the hook, so unreadable favorites are hidden — while still showing everything in layout-customization mode (consistent with the Workspace section). WorkspaceSectionContainer refactored to consume the same hook, removing its inline isItemReadable, the dual-map reduce, and inline filtering. ## Before ### With access Screenshot 2026-06-25 at 14 01 38 ### Without access Screenshot 2026-06-25 at 14 02 08 ## After ### Without access Screenshot 2026-06-25 at 14 02 46 Review in cubic --- .../hooks/useReadableNavigationMenuItems.ts | 60 +++++++++++++++++++ .../favorites/components/FavoritesSection.tsx | 13 +++- .../components/WorkspaceSectionContainer.tsx | 46 ++++---------- 3 files changed, 83 insertions(+), 36 deletions(-) create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useReadableNavigationMenuItems.ts diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useReadableNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useReadableNavigationMenuItems.ts new file mode 100644 index 0000000000..61949c136e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useReadableNavigationMenuItems.ts @@ -0,0 +1,60 @@ +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { isNavigationMenuItemReadable } from '@/navigation-menu-item/common/utils/isNavigationMenuItemReadable'; +import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { viewsSelector } from '@/views/states/selectors/viewsSelector'; + +type UseReadableNavigationMenuItemsArgs = { + topLevelItems: NavigationMenuItem[]; + folderChildrenById: Map; +}; + +export const useReadableNavigationMenuItems = ({ + topLevelItems, + folderChildrenById, +}: UseReadableNavigationMenuItemsArgs) => { + const isLayoutCustomizationModeEnabled = useAtomStateValue( + isLayoutCustomizationModeEnabledState, + ); + const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); + const views = useAtomStateValue(viewsSelector); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const isItemReadable = (item: NavigationMenuItem) => + isNavigationMenuItemReadable({ + item, + objectMetadataItems, + views, + objectPermissionsByObjectMetadataId, + }); + + const filteredFolderChildrenById = new Map(); + for (const [folderId, children] of folderChildrenById) { + filteredFolderChildrenById.set(folderId, children.filter(isItemReadable)); + } + + const filteredTopLevelItems = topLevelItems.filter((item) => + isNavigationMenuItemFolder(item) + ? (filteredFolderChildrenById.get(item.id) ?? []).length > 0 + : isItemReadable(item), + ); + + const displayTopLevelItems = isLayoutCustomizationModeEnabled + ? topLevelItems + : filteredTopLevelItems; + const displayFolderChildrenById = isLayoutCustomizationModeEnabled + ? folderChildrenById + : filteredFolderChildrenById; + + return { + isItemReadable, + filteredTopLevelItems, + filteredFolderChildrenById, + displayTopLevelItems, + displayFolderChildrenById, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx index 489269b4d1..dedb56fe4e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx @@ -20,6 +20,7 @@ import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/ import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection'; import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; +import { useReadableNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useReadableNavigationMenuItems'; import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget'; import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection'; @@ -71,12 +72,12 @@ export const FavoritesSection = () => { 'Favorites', ); - const topLevelItems = useMemo( + const allTopLevelItems = useMemo( () => navigationMenuItemsSorted.filter((item) => !item.folderId), [navigationMenuItemsSorted], ); - const folderChildrenById = useMemo(() => { + const allFolderChildrenById = useMemo(() => { const map = new Map(); for (const folder of userNavigationMenuItemsByFolder) { map.set(folder.id, folder.navigationMenuItems); @@ -84,6 +85,14 @@ export const FavoritesSection = () => { return map; }, [userNavigationMenuItemsByFolder]); + const { + displayTopLevelItems: topLevelItems, + displayFolderChildrenById: folderChildrenById, + } = useReadableNavigationMenuItems({ + topLevelItems: allTopLevelItems, + folderChildrenById: allFolderChildrenById, + }); + const folderCount = useMemo( () => topLevelItems.filter(isNavigationMenuItemFolder).length, [topLevelItems], diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx index e727e91253..bd25a0e73e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx @@ -7,9 +7,8 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds'; import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { isNavigationMenuItemReadable } from '@/navigation-menu-item/common/utils/isNavigationMenuItemReadable'; import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; +import { useReadableNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useReadableNavigationMenuItems'; import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection'; import { WorkspaceSectionListReadOnly } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly'; @@ -17,7 +16,6 @@ import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; @@ -59,7 +57,6 @@ export const WorkspaceSectionContainer = ({ useNavigationSection('Workspace'); const views = useAtomStateValue(viewsSelector); - const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const { addToNavigationFallbackDestination } = useContext( NavigationDropTargetContext, @@ -69,46 +66,27 @@ export const WorkspaceSectionContainer = ({ const isAddToNavigationDropTargetVisible = addToNavigationFallbackDestination?.droppableId === NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS; - const isItemReadable = (item: NavigationMenuItem) => - isNavigationMenuItemReadable({ - item, - objectMetadataItems, - views, - objectPermissionsByObjectMetadataId, - }); - const { folderChildrenById, filteredFolderChildrenById } = items.reduce<{ - folderChildrenById: Map; - filteredFolderChildrenById: Map; - }>( + const folderChildrenById = items.reduce>( (acc, item) => { const folderId = item.folderId; if (isDefined(folderId)) { - const children = acc.folderChildrenById.get(folderId) ?? []; + const children = acc.get(folderId) ?? []; children.push(item); - acc.folderChildrenById.set(folderId, children); - - if (isItemReadable(item)) { - const readableChildren = - acc.filteredFolderChildrenById.get(folderId) ?? []; - readableChildren.push(item); - acc.filteredFolderChildrenById.set(folderId, readableChildren); - } + acc.set(folderId, children); } return acc; }, - { - folderChildrenById: new Map(), - filteredFolderChildrenById: new Map(), - }, + new Map(), ); - const filteredItems = flatItems.filter((item) => { - if (isNavigationMenuItemFolder(item)) { - return (filteredFolderChildrenById.get(item.id) ?? []).length > 0; - } - return isItemReadable(item); - }); + const { filteredTopLevelItems, filteredFolderChildrenById } = + useReadableNavigationMenuItems({ + topLevelItems: flatItems, + folderChildrenById, + }); + + const filteredItems = filteredTopLevelItems; const workspaceOrphanItemsForSection = isLayoutCustomizationModeEnabled ? flatItems