From 918fdcbf87cd0cf82cdbe8cfe271e7c483be12e0 Mon Sep 17 00:00:00 2001 From: Abdul Rahman Date: Fri, 3 Apr 2026 11:05:21 +0530 Subject: [PATCH] Enhance navigation menu item matching logic - Updated `isLocationMatchingNavigationMenuItem` to include additional parameters for better matching of object record show paths. - Introduced logging for debugging navigation matches, capturing relevant data for analysis. - Added tests to ensure correct behavior for object navigation items on record show pages, distinguishing between view-only and object pins. - Refactored related components to utilize the updated matching logic. This improves the accuracy of navigation item states and enhances debugging capabilities. --- ...LocationMatchingNavigationMenuItem.test.ts | 32 +++++++++++++++++++ .../isLocationMatchingNavigationMenuItem.ts | 27 ++++++++++++++-- .../useNavigationMenuItemFolderOpenState.ts | 8 +++++ ...igationDrawerItemForObjectMetadataItem.tsx | 16 +++++++--- 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts index 26a41c0aec0..200a94a633f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts @@ -39,4 +39,36 @@ describe('isLocationMatchingNavigationMenuItem', () => { ), ).toBe(false); }); + + it('should match object or view nav item when on a record show page for that object', () => { + expect( + isLocationMatchingNavigationMenuItem( + '/object/company/rec-1', + '/object/company/rec-1', + NavigationMenuItemType.OBJECT, + '/objects/companies?viewId=idx', + 'company', + ), + ).toBe(true); + + expect( + isLocationMatchingNavigationMenuItem( + '/object/person/rec-1', + '/object/person/rec-1', + NavigationMenuItemType.VIEW, + '/objects/people?viewId=v1', + 'person', + ), + ).toBe(true); + + expect( + isLocationMatchingNavigationMenuItem( + '/object/person/rec-1', + '/object/person/rec-1', + NavigationMenuItemType.OBJECT, + '/objects/companies?viewId=idx', + 'company', + ), + ).toBe(false); + }); }); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts index 1dcee88bc67..b25fa17658f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts @@ -1,15 +1,38 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isNonEmptyString } from '@sniptt/guards'; + +import { AppPath, NavigationMenuItemType } from 'twenty-shared/types'; +import { getAppPath } from 'twenty-shared/utils'; export const isLocationMatchingNavigationMenuItem = ( currentPath: string, currentViewPath: string, navigationMenuItemType: NavigationMenuItemType, computedLink: string, + objectNameSingularForRecordShowMatch: string | null = null, ) => { const isViewBasedItem = navigationMenuItemType === NavigationMenuItemType.VIEW || navigationMenuItemType === NavigationMenuItemType.OBJECT; - return isViewBasedItem + + const matchesListDestination = isViewBasedItem ? computedLink === currentViewPath : computedLink === currentPath; + + if (matchesListDestination) { + return true; + } + + if ( + isViewBasedItem && + isNonEmptyString(objectNameSingularForRecordShowMatch) + ) { + const recordShowBase = getAppPath(AppPath.RecordShowPage, { + objectNameSingular: objectNameSingularForRecordShowMatch, + objectRecordId: '', + }); + + return currentPath.startsWith(`${recordShowBase}/`); + } + + return false; }; 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 075c23dccd8..95629346209 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 @@ -6,6 +6,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; @@ -85,11 +86,18 @@ export const useNavigationMenuItemFolderOpenState = ({ objectMetadataItems, views, ); + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + item, + objectMetadataItems, + views, + ); + return isLocationMatchingNavigationMenuItem( currentPath, currentViewPath, item.type, computedLink, + objectMetadataItem?.nameSingular ?? null, ); }, ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx index 31c053f4eaf..371bbbb5abd 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { Fragment, type ReactNode, useContext } from 'react'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; @@ -91,17 +92,22 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ const computedLink = hasCustomLink ? navigationPath : ''; const isActive = hasCustomLink - ? (isView || isObject ? currentPathWithSearch : currentPath) === - computedLink + ? isLocationMatchingNavigationMenuItem( + currentPath, + currentPathWithSearch, + navigationMenuItem!.type, + computedLink, + isObject || isView ? objectMetadataItem.nameSingular : null, + ) : currentPath === getAppPath(AppPath.RecordIndexPage, { objectNamePlural: objectMetadataItem.namePlural, }) || - currentPath.includes( - getAppPath(AppPath.RecordShowPage, { + currentPath.startsWith( + `${getAppPath(AppPath.RecordShowPage, { objectNameSingular: objectMetadataItem.nameSingular, objectRecordId: '', - }) + '/', + })}/`, ); const handleClick = isLayoutCustomizationModeEnabled