diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts index f35dbdba746..0d07339e023 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState.ts @@ -1,16 +1,14 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useLocation, useNavigate } from 'react-router-dom'; import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; import { useIsMobile } from 'twenty-ui/utilities'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState'; import { currentNavigationMenuItemFolderIdState } from '@/navigation-menu-item/common/states/currentNavigationMenuItemFolderIdState'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; -import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; -import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject'; -import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { resolveFolderSelectedNavigationMenuItemIndex } from '@/navigation-menu-item/display/folder/utils/resolveFolderSelectedNavigationMenuItemIndex'; +import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; @@ -34,6 +32,7 @@ export const useNavigationMenuItemFolderOpenState = ({ const isMobile = useIsMobile(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); + const { workspaceNavigationMenuItems } = useNavigationMenuItemsData(); const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] = useAtomState(openNavigationMenuItemFolderIdsState); @@ -76,89 +75,16 @@ export const useNavigationMenuItemFolderOpenState = ({ const activeNavigationItem = useAtomStateValue(activeNavigationItemState); - const explicitMatchIndex = isDefined(activeNavigationItem) - ? navigationMenuItems.findIndex( - (item) => item.id === activeNavigationItem.navItemId, - ) - : -1; - - const activeNavigationObjectNameSingular = - activeNavigationItem?.objectNameSingular; - - const navigationMenuItemMatchesActiveObject = ( - navigationMenuItem: NavigationMenuItem, - ): boolean => { - const objectMetadataItem = getObjectMetadataForNavigationMenuItem( - navigationMenuItem, + const selectedNavigationMenuItemIndex = + resolveFolderSelectedNavigationMenuItemIndex({ + navigationMenuItems, + activeNavigationItem, + currentPath, + currentViewPath, objectMetadataItems, views, - ); - - if (!isDefined(objectMetadataItem)) { - return false; - } - - return ( - objectMetadataItem.nameSingular === activeNavigationObjectNameSingular - ); - }; - - const isActiveNavigationItemObjectInFolder = - isDefined(activeNavigationObjectNameSingular) && - navigationMenuItems.some(navigationMenuItemMatchesActiveObject); - - const recordMatchIndex = navigationMenuItems.findIndex((item) => { - if (item.type !== NavigationMenuItemType.RECORD) { - return false; - } - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - return computedLink === currentPath; - }); - - const urlMatchIndex = navigationMenuItems.findIndex((item) => { - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - - if ( - isLocationMatchingNavigationMenuItem( - currentPath, - currentViewPath, - item.type, - computedLink, - ) - ) { - return true; - } - - if (item.type === NavigationMenuItemType.OBJECT) { - const objectMetadataItem = getObjectMetadataForNavigationMenuItem( - item, - objectMetadataItems, - views, - ); - if (isDefined(objectMetadataItem)) { - return matchesRecordShowPathForObject( - currentPath, - objectMetadataItem.nameSingular, - ); - } - } - - return false; - }); - - const selectedNavigationMenuItemIndex = isActiveNavigationItemObjectInFolder - ? explicitMatchIndex !== -1 - ? explicitMatchIndex - : recordMatchIndex - : urlMatchIndex; + workspaceNavigationMenuItems, + }); return { isOpen, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/doesFolderNavigationMenuItemMatchUrlForSelection.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/doesFolderNavigationMenuItemMatchUrlForSelection.ts new file mode 100644 index 00000000000..8431ee598d0 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/doesFolderNavigationMenuItemMatchUrlForSelection.ts @@ -0,0 +1,110 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; +import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { type View } from '@/views/types/View'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +type DoesFolderNavigationMenuItemMatchUrlForSelectionParams = { + folderChildNavigationMenuItem: NavigationMenuItem; + workspaceNavigationMenuItems: NavigationMenuItem[]; + currentPath: string; + currentViewPath: string; + objectMetadataItems: EnrichedObjectMetadataItem[]; + views: Pick[]; +}; + +export const doesFolderNavigationMenuItemMatchUrlForSelection = ({ + folderChildNavigationMenuItem, + workspaceNavigationMenuItems, + currentPath, + currentViewPath, + objectMetadataItems, + views, +}: DoesFolderNavigationMenuItemMatchUrlForSelectionParams): boolean => { + const computedLink = getNavigationMenuItemComputedLink( + folderChildNavigationMenuItem, + objectMetadataItems, + views, + ); + + let locationMatches = isLocationMatchingNavigationMenuItem( + currentPath, + currentViewPath, + folderChildNavigationMenuItem.type, + computedLink, + ); + + if ( + !locationMatches && + folderChildNavigationMenuItem.type === NavigationMenuItemType.OBJECT + ) { + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + folderChildNavigationMenuItem, + objectMetadataItems, + views, + ); + if (isDefined(objectMetadataItem)) { + locationMatches = matchesRecordShowPathForObject( + currentPath, + objectMetadataItem.nameSingular, + ); + } + } + + if (!locationMatches) { + return false; + } + + const objectMetadataForFolderChild = getObjectMetadataForNavigationMenuItem( + folderChildNavigationMenuItem, + objectMetadataItems, + views, + ); + + if (!isDefined(objectMetadataForFolderChild)) { + return true; + } + + return !workspaceNavigationMenuItems.some((workspaceItem) => { + if ( + isDefined(workspaceItem.folderId) || + workspaceItem.type !== NavigationMenuItemType.OBJECT + ) { + return false; + } + + const topLevelComputedLink = getNavigationMenuItemComputedLink( + workspaceItem, + objectMetadataItems, + views, + ); + + if ( + !isLocationMatchingNavigationMenuItem( + currentPath, + currentViewPath, + workspaceItem.type, + topLevelComputedLink, + ) + ) { + return false; + } + + const objectMetadataForTopLevelItem = + getObjectMetadataForNavigationMenuItem( + workspaceItem, + objectMetadataItems, + views, + ); + + return ( + isDefined(objectMetadataForTopLevelItem) && + objectMetadataForTopLevelItem.id === objectMetadataForFolderChild.id + ); + }); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/resolveFolderSelectedNavigationMenuItemIndex.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/resolveFolderSelectedNavigationMenuItemIndex.ts new file mode 100644 index 00000000000..4de02e776a0 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/utils/resolveFolderSelectedNavigationMenuItemIndex.ts @@ -0,0 +1,90 @@ +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { doesFolderNavigationMenuItemMatchUrlForSelection } from '@/navigation-menu-item/display/folder/utils/doesFolderNavigationMenuItemMatchUrlForSelection'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; +import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { type View } from '@/views/types/View'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +type ActiveNavigationItemForFolderSelection = { + navItemId: string; + objectNameSingular: string; +}; + +type ResolveFolderSelectedNavigationMenuItemIndexParams = { + navigationMenuItems: NavigationMenuItem[]; + activeNavigationItem: ActiveNavigationItemForFolderSelection | null; + currentPath: string; + currentViewPath: string; + objectMetadataItems: EnrichedObjectMetadataItem[]; + views: Pick[]; + workspaceNavigationMenuItems: NavigationMenuItem[]; +}; + +export const resolveFolderSelectedNavigationMenuItemIndex = ({ + navigationMenuItems, + activeNavigationItem, + currentPath, + currentViewPath, + objectMetadataItems, + views, + workspaceNavigationMenuItems, +}: ResolveFolderSelectedNavigationMenuItemIndexParams): number => { + const explicitMatchIndex = isDefined(activeNavigationItem) + ? navigationMenuItems.findIndex( + (item) => item.id === activeNavigationItem.navItemId, + ) + : -1; + + const activeNavigationObjectNameSingular = + activeNavigationItem?.objectNameSingular; + + const isActiveNavigationItemObjectInFolder = + isDefined(activeNavigationObjectNameSingular) && + navigationMenuItems.some((navigationMenuItem) => { + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + navigationMenuItem, + objectMetadataItems, + views, + ); + + if (!isDefined(objectMetadataItem)) { + return false; + } + + return ( + objectMetadataItem.nameSingular === activeNavigationObjectNameSingular + ); + }); + + const recordMatchIndex = navigationMenuItems.findIndex((item) => { + if (item.type !== NavigationMenuItemType.RECORD) { + return false; + } + const computedLink = getNavigationMenuItemComputedLink( + item, + objectMetadataItems, + views, + ); + return computedLink === currentPath; + }); + + const urlMatchIndex = navigationMenuItems.findIndex((folderChildItem) => + doesFolderNavigationMenuItemMatchUrlForSelection({ + folderChildNavigationMenuItem: folderChildItem, + workspaceNavigationMenuItems, + currentPath, + currentViewPath, + objectMetadataItems, + views, + }), + ); + + return isActiveNavigationItemObjectInFolder + ? explicitMatchIndex !== -1 + ? explicitMatchIndex + : recordMatchIndex + : urlMatchIndex; +};