From 762fb6fd642e0bd4314af244e41f7796cc706a7c Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:30:47 +0530 Subject: [PATCH] Fix active navigation item disambiguation (#19664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Introduce a `activeNavigationMenuItemState` Jotai atom (persisted via localStorage) to disambiguate active navigation items when multiple items share the same URL - Add active item evaluation for record show pages with three scenarios: 1. Navigating from a nav item → parent stays active + dedicated RECORD item also active 2. Clicking a dedicated RECORD nav item → only that item active 3. Navigating via search/direct link → OBJECT nav item fallback, or Opened section if none exists - Extract shared active logic into `isNavigationMenuItemActive` utility to eliminate duplication between orphan items and folder items - Support multiple simultaneously active items within folders via `Set` instead of a single index --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../lastClickedNavigationMenuItemIdState.ts | 9 + ...LocationMatchingNavigationMenuItem.test.ts | 42 ---- .../isLocationMatchingNavigationMenuItem.ts | 15 -- .../components/NavigationMenuItemFolder.tsx | 19 +- .../NavigationMenuItemFolderDnd.tsx | 16 +- .../NavigationMenuItemFolderSubItem.tsx | 32 ++- .../useNavigationMenuItemFolderOpenState.ts | 61 +++--- .../useIdentifyActiveNavigationMenuItems.ts | 182 ++++++++++++++++++ .../hooks/useWorkspaceNavigationMenuItems.ts | 21 -- ...igationDrawerItemForObjectMetadataItem.tsx | 44 ++--- .../NavigationDrawerOpenedSection.tsx | 21 +- .../state/jotai/utils/createAtomState.ts | 9 +- 12 files changed, 296 insertions(+), 175 deletions(-) create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState.ts delete mode 100644 packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts delete mode 100644 packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems.ts delete mode 100644 packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems.ts diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState.ts new file mode 100644 index 0000000000..3699807417 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState.ts @@ -0,0 +1,9 @@ +import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; + +export const lastClickedNavigationMenuItemIdState = createAtomState< + string | null +>({ + key: 'lastClickedNavigationMenuItemIdState', + defaultValue: null, + useSessionStorage: true, +}); 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 deleted file mode 100644 index 26a41c0aec..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/isLocationMatchingNavigationMenuItem.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem'; - -describe('isLocationMatchingNavigationMenuItem', () => { - it('should return true when item link matches current path (non-view) or current view path (view)', () => { - expect( - isLocationMatchingNavigationMenuItem( - '/app/objects/people', - '/app/objects/people?viewId=123', - NavigationMenuItemType.RECORD, - '/app/objects/people', - ), - ).toBe(true); - expect( - isLocationMatchingNavigationMenuItem( - '/app/objects/companies', - '/app/objects/companies?viewId=123', - NavigationMenuItemType.VIEW, - '/app/objects/companies?viewId=123', - ), - ).toBe(true); - }); - - it('should return false when item link does not match path', () => { - expect( - isLocationMatchingNavigationMenuItem( - '/app/objects/people', - '/app/objects/people?viewId=123', - NavigationMenuItemType.RECORD, - '/app/objects/company', - ), - ).toBe(false); - expect( - isLocationMatchingNavigationMenuItem( - '/app/objects/companies', - '/app/objects/companies?viewId=123', - NavigationMenuItemType.VIEW, - '/app/objects/companies?viewId=456', - ), - ).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 deleted file mode 100644 index 1dcee88bc6..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; - -export const isLocationMatchingNavigationMenuItem = ( - currentPath: string, - currentViewPath: string, - navigationMenuItemType: NavigationMenuItemType, - computedLink: string, -) => { - const isViewBasedItem = - navigationMenuItemType === NavigationMenuItemType.VIEW || - navigationMenuItemType === NavigationMenuItemType.OBJECT; - return isViewBasedItem - ? computedLink === currentViewPath - : computedLink === currentPath; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx index e1b16be1a6..b07b1e2537 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolder.tsx @@ -47,7 +47,8 @@ export const NavigationMenuItemFolder = ({ const folderName = item.name ?? 'Folder'; const folderIconKey = item.icon; const folderColor = 'color' in item ? (item.color as string | null) : null; - const navigationMenuItems = folderChildrenById.get(folderId) ?? []; + const folderChildrenNavigationMenuItems = + folderChildrenById.get(folderId) ?? []; const isGroup = folderCount > 1; if (readOnly) { @@ -57,7 +58,7 @@ export const NavigationMenuItemFolder = ({ folderName={folderName} folderIconKey={folderIconKey} folderColor={folderColor} - navigationMenuItems={navigationMenuItems} + navigationMenuItems={folderChildrenNavigationMenuItems} isGroup={isGroup} /> ); @@ -71,7 +72,7 @@ export const NavigationMenuItemFolder = ({ folderName={folderName} folderIconKey={folderIconKey} folderColor={folderColor} - navigationMenuItems={navigationMenuItems} + navigationMenuItems={folderChildrenNavigationMenuItems} isGroup={isGroup} /> } @@ -81,7 +82,7 @@ export const NavigationMenuItemFolder = ({ folderName={folderName} folderIconKey={folderIconKey} folderColor={folderColor} - navigationMenuItems={navigationMenuItems} + navigationMenuItems={folderChildrenNavigationMenuItems} isGroup={isGroup} isEditInPlace={isEditInPlace} editModeProps={editModeProps} @@ -115,8 +116,11 @@ const NavigationMenuItemFolderReadOnlyContent = ({ const { theme } = useContext(ThemeContext); const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); - const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = - useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems }); + const { isOpen, handleToggle, hasActiveChild } = + useNavigationMenuItemFolderOpenState({ + folderId, + folderChildrenNavigationMenuItems: navigationMenuItems, + }); return ( = 0} + active={!isOpen && hasActiveChild} onClick={handleToggle} className="navigation-drawer-item" triggerEvent="CLICK" @@ -161,7 +165,6 @@ const NavigationMenuItemFolderReadOnlyContent = ({ navigationMenuItem={navigationMenuItem} index={index} arrayLength={navigationMenuItems.length} - selectedNavigationMenuItemIndex={selectedNavigationMenuItemIndex} isDragging={false} /> ))} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx index 1de6e89e6e..0778d6b7e6 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx @@ -39,8 +39,8 @@ import { NavigationMenuItemFolderLayout } from '@/navigation-menu-item/display/f import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderNavigationDrawerItemDropdown'; import { NavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem'; import { useNavigationMenuItemFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState'; -import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { useIsNavigationMenuItemEditHighlighted } from '@/navigation-menu-item/display/hooks/useIsNavigationMenuItemEditHighlighted'; +import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { useFavoritesFolderEdit } from '@/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit'; import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage'; import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; @@ -116,8 +116,11 @@ export const NavigationMenuItemFolderDnd = ({ ? NavigationSections.FAVORITES : NavigationSections.WORKSPACE; - const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = - useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems }); + const { isOpen, handleToggle, hasActiveChild } = + useNavigationMenuItemFolderOpenState({ + folderId, + folderChildrenNavigationMenuItems: navigationMenuItems, + }); const { isDragging: isContextDragging } = useContext( NavigationMenuItemDragContext, @@ -228,7 +231,7 @@ export const NavigationMenuItemFolderDnd = ({ Icon={FolderIcon} iconColor={iconColor} active={ - (!isOpen && selectedNavigationMenuItemIndex >= 0) || + (!isOpen && hasActiveChild) || (isWorkspace && isSelectedInEditMode && !isOpen) } onClick={handleHeaderClick} @@ -348,9 +351,6 @@ export const NavigationMenuItemFolderDnd = ({ navigationMenuItem={navigationMenuItem} index={index} arrayLength={folderContentLength} - selectedNavigationMenuItemIndex={ - selectedNavigationMenuItemIndex - } isDragging={isDragging} rightOptions={ isEditInPlace ? ( @@ -399,7 +399,7 @@ export const NavigationMenuItemFolderDnd = ({ subItemState={getNavigationSubItemLeftAdornment({ index: navigationMenuItems.length, arrayLength: folderContentLength, - selectedIndex: selectedNavigationMenuItemIndex, + selectedIndex: -1, })} /> )} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx index 67f7c4cb8e..dc2d6b73f3 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem.tsx @@ -1,28 +1,31 @@ import { type ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; import { NavigationMenuItemType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; +import { lastClickedNavigationMenuItemIdState } from '@/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState'; import { getNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getNavigationMenuItemColor'; import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon'; -import type { EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; -import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { useIdentifyActiveNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems'; +import { useIsNavigationMenuItemEditHighlighted } from '@/navigation-menu-item/display/hooks/useIsNavigationMenuItemEditHighlighted'; import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; -import { useIsNavigationMenuItemEditHighlighted } from '@/navigation-menu-item/display/hooks/useIsNavigationMenuItemEditHighlighted'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; +import type { EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; type NavigationMenuItemFolderSubItemProps = { navigationMenuItem: NavigationMenuItem; index: number; arrayLength: number; - selectedNavigationMenuItemIndex: number; isDragging: boolean; rightOptions?: ReactNode; onClick?: () => void; @@ -36,7 +39,6 @@ export const NavigationMenuItemFolderSubItem = ({ navigationMenuItem, index, arrayLength, - selectedNavigationMenuItemIndex, isDragging, rightOptions, onClick, @@ -46,6 +48,15 @@ export const NavigationMenuItemFolderSubItem = ({ useIsNavigationMenuItemEditHighlighted(navigationMenuItem); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); + const navigate = useNavigate(); + const setLastClickedNavigationMenuItemId = useSetAtomState( + lastClickedNavigationMenuItemIdState, + ); + + const { activeNavigationMenuItemIds } = + useIdentifyActiveNavigationMenuItems(); + + const isActive = activeNavigationMenuItemIds.includes(navigationMenuItem.id); const label = getNavigationMenuItemLabel( navigationMenuItem, @@ -87,7 +98,10 @@ export const NavigationMenuItemFolderSubItem = ({ item: navigationMenuItem, objectMetadataItem: objectMetadataItem ?? undefined, }) - : undefined); + : () => { + setLastClickedNavigationMenuItemId(navigationMenuItem.id); + navigate(computedLink); + }); return ( { - const location = useLocation(); const navigate = useNavigate(); - const currentPath = location.pathname; - const currentViewPath = location.pathname + location.search; const isMobile = useIsMobile(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); @@ -38,24 +36,16 @@ export const useNavigationMenuItemFolderOpenState = ({ currentNavigationMenuItemFolderIdState, ); - const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex( - (item) => { - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - return isLocationMatchingNavigationMenuItem( - currentPath, - currentViewPath, - item.type, - computedLink, - ); - }, + const { activeNavigationMenuItemIds } = + useIdentifyActiveNavigationMenuItems(); + const setLastClickedNavigationMenuItemId = useSetAtomState( + lastClickedNavigationMenuItemIdState, ); const isExplicitlyOpen = openNavigationMenuItemFolderIds.includes(folderId); - const hasActiveChild = selectedNavigationMenuItemIndex >= 0; + const hasActiveChild = folderChildrenNavigationMenuItems.some((item) => + activeNavigationMenuItemIds.includes(item.id), + ); const isOpen = isExplicitlyOpen || hasActiveChild; const handleToggle = () => { @@ -72,17 +62,19 @@ export const useNavigationMenuItemFolderOpenState = ({ } if (!isOpen) { - const firstNonLinkItem = navigationMenuItems.find((item) => { - if (item.type === NavigationMenuItemType.LINK) { - return false; - } - const computedLink = getNavigationMenuItemComputedLink( - item, - objectMetadataItems, - views, - ); - return isNonEmptyString(computedLink); - }); + const firstNonLinkItem = folderChildrenNavigationMenuItems.find( + (item) => { + if (item.type === NavigationMenuItemType.LINK) { + return false; + } + const computedLink = getNavigationMenuItemComputedLink( + item, + objectMetadataItems, + views, + ); + return isNonEmptyString(computedLink); + }, + ); if (isDefined(firstNonLinkItem)) { const link = getNavigationMenuItemComputedLink( firstNonLinkItem, @@ -90,6 +82,7 @@ export const useNavigationMenuItemFolderOpenState = ({ views, ); if (isNonEmptyString(link)) { + setLastClickedNavigationMenuItemId(firstNonLinkItem.id); navigate(link); } } @@ -99,6 +92,6 @@ export const useNavigationMenuItemFolderOpenState = ({ return { isOpen, handleToggle, - selectedNavigationMenuItemIndex, + hasActiveChild, }; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems.ts new file mode 100644 index 0000000000..f86326ee6a --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems.ts @@ -0,0 +1,182 @@ +import { useMemo } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { AppPath, NavigationMenuItemType } from 'twenty-shared/types'; +import { getAppPath, isDefined } from 'twenty-shared/utils'; + +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { lastClickedNavigationMenuItemIdState } from '@/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState'; +import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem'; +import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { viewsSelector } from '@/views/states/selectors/viewsSelector'; + +export const useIdentifyActiveNavigationMenuItems = (): { + activeNavigationMenuItemIds: string[]; + objectMetadataIdForOpenedSection: string | null; +} => { + const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector); + const lastClickedNavigationMenuItemId = useAtomStateValue( + lastClickedNavigationMenuItemIdState, + ); + const views = useAtomStateValue(viewsSelector); + const { activeObjectMetadataItems, objectMetadataItems } = + useFilteredObjectMetadataItems(); + + const location = useLocation(); + const { + objectNamePlural: currentObjectNamePlural, + objectNameSingular: currentObjectNameSingular, + } = useParams(); + + const currentPath = location.pathname; + const currentPathWithSearch = location.pathname + location.search; + + const currentObjectMetadataItem = activeObjectMetadataItems.find( + (item) => + item.namePlural === currentObjectNamePlural || + item.nameSingular === currentObjectNameSingular, + ); + + const isOnRecordShowPage = + isDefined(currentObjectMetadataItem) && + currentPath.includes( + getAppPath(AppPath.RecordShowPage, { + objectNameSingular: currentObjectMetadataItem.nameSingular, + objectRecordId: '', + }) + '/', + ); + + const contextStoreCurrentViewId = useAtomComponentStateValue( + contextStoreCurrentViewIdComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + + const { activeNavigationMenuItemIds, objectMetadataIdForOpenedSection } = + useMemo(() => { + if (isDefined(lastClickedNavigationMenuItemId)) { + const lastClickedItem = navigationMenuItems.find( + (item) => item.id === lastClickedNavigationMenuItemId, + ); + + if (isDefined(lastClickedItem)) { + const lastClickedNavigationMenuItemLink = + getNavigationMenuItemComputedLink( + lastClickedItem, + objectMetadataItems, + views, + ); + const lastClickedObjectMetadataId = + getObjectMetadataForNavigationMenuItem( + lastClickedItem, + objectMetadataItems, + views, + )?.id; + + const pathMatches = + currentPathWithSearch === lastClickedNavigationMenuItemLink; + const objectMatchesOnShowPage = + isOnRecordShowPage && + isDefined(lastClickedObjectMetadataId) && + lastClickedObjectMetadataId === currentObjectMetadataItem?.id; + + const isLastClickedNavigationMenuItemRelevant = + pathMatches || objectMatchesOnShowPage; + if (isLastClickedNavigationMenuItemRelevant) { + return { + activeNavigationMenuItemIds: [lastClickedItem.id], + objectMetadataIdForOpenedSection: null, + }; + } + } + } + + if (isOnRecordShowPage) { + const matchingRecordNavigationMenuItemIds = navigationMenuItems + .filter((item) => { + if (item.type !== NavigationMenuItemType.RECORD) { + return false; + } + const link = getNavigationMenuItemComputedLink( + item, + objectMetadataItems, + views, + ); + return link === currentPath; + }) + .map((item) => item.id); + + const matchingObjectNavigationMenuItemIds = navigationMenuItems + .filter((item) => { + if (item.type !== NavigationMenuItemType.OBJECT) { + return false; + } + const itemObjectMetadataId = getObjectMetadataForNavigationMenuItem( + item, + objectMetadataItems, + views, + )?.id; + return itemObjectMetadataId === currentObjectMetadataItem?.id; + }) + .map((item) => item.id); + + const activeNavigationMenuItemIds = [ + ...matchingRecordNavigationMenuItemIds, + ...matchingObjectNavigationMenuItemIds, + ]; + + return { + activeNavigationMenuItemIds, + objectMetadataIdForOpenedSection: + activeNavigationMenuItemIds.length === 0 + ? currentObjectMetadataItem?.id + : null, + }; + } + + const matchingViewNavigationMenuItemIds = navigationMenuItems + .filter( + (item) => + item.type === NavigationMenuItemType.VIEW && + isDefined(contextStoreCurrentViewId) && + item.viewId === contextStoreCurrentViewId, + ) + .map((item) => item.id); + + if (matchingViewNavigationMenuItemIds.length > 0) { + return { + activeNavigationMenuItemIds: matchingViewNavigationMenuItemIds, + objectMetadataIdForOpenedSection: null, + }; + } + + const matchingObjectNavigationMenuItemIds = navigationMenuItems + .filter( + (item) => + item.type === NavigationMenuItemType.OBJECT && + item.targetObjectMetadataId === currentObjectMetadataItem?.id, + ) + .map((item) => item.id); + + return { + activeNavigationMenuItemIds: matchingObjectNavigationMenuItemIds, + objectMetadataIdForOpenedSection: + matchingObjectNavigationMenuItemIds.length === 0 && + isDefined(currentObjectMetadataItem) + ? currentObjectMetadataItem.id + : null, + }; + }, [ + navigationMenuItems, + lastClickedNavigationMenuItemId, + objectMetadataItems, + views, + location, + contextStoreCurrentViewId, + ]); + + return { activeNavigationMenuItemIds, objectMetadataIdForOpenedSection }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems.ts deleted file mode 100644 index 38258e7f36..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { useNavigationMenuItemsData } from './useNavigationMenuItemsData'; - -export const useWorkspaceNavigationMenuItems = (): { - objectMetadataIdsInWorkspaceNav: Set; -} => { - const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } = - useNavigationMenuItemsData(); - - const objectMetadataIdsInWorkspaceNav = new Set( - rawWorkspaceNavigationMenuItems - .filter((item) => item.type === NavigationMenuItemType.OBJECT) - .map((item) => item.targetObjectMetadataId) - .filter((objectMetadataId) => isDefined(objectMetadataId)), - ); - - return { - objectMetadataIdsInWorkspaceNav, - }; -}; 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 31c053f4ea..097ca4f114 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,7 +3,9 @@ import { isNonEmptyString } from '@sniptt/guards'; import { Fragment, type ReactNode, useContext } from 'react'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { lastClickedNavigationMenuItemIdState } from '@/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState'; import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier'; +import { useIdentifyActiveNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; @@ -15,8 +17,9 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -import { useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { AppPath, CoreObjectNameSingular, @@ -67,16 +70,20 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ const { getIcon } = useIcons(); const objectNavItemColor = getObjectColorWithFallback(objectMetadataItem); - const location = useLocation(); - const currentPath = location.pathname; - const currentPathWithSearch = `${location.pathname}${location.search}`; + const navigate = useNavigate(); + + const { activeNavigationMenuItemIds, objectMetadataIdForOpenedSection } = + useIdentifyActiveNavigationMenuItems(); + const setLastClickedNavigationMenuItemId = useSetAtomState( + lastClickedNavigationMenuItemIdState, + ); const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD; const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW; const isObject = navigationMenuItem?.type === NavigationMenuItemType.OBJECT; - const hasCustomLink = isRecord || isView || isObject; + const hasNavigationMenuItem = isRecord || isView || isObject; - const navigationPath = hasCustomLink + const navigationPath = hasNavigationMenuItem ? getNavigationMenuItemComputedLink( navigationMenuItem!, objectMetadataItems, @@ -88,25 +95,18 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ lastVisitedViewId ? { viewId: lastVisitedViewId } : undefined, ); - const computedLink = hasCustomLink ? navigationPath : ''; - - const isActive = hasCustomLink - ? (isView || isObject ? currentPathWithSearch : currentPath) === - computedLink - : currentPath === - getAppPath(AppPath.RecordIndexPage, { - objectNamePlural: objectMetadataItem.namePlural, - }) || - currentPath.includes( - getAppPath(AppPath.RecordShowPage, { - objectNameSingular: objectMetadataItem.nameSingular, - objectRecordId: '', - }) + '/', - ); + const isActive = hasNavigationMenuItem + ? activeNavigationMenuItemIds.includes(navigationMenuItem!.id) + : objectMetadataIdForOpenedSection === objectMetadataItem.id; const handleClick = isLayoutCustomizationModeEnabled ? onEditModeClick - : undefined; + : hasNavigationMenuItem && !isDragging + ? () => { + setLastClickedNavigationMenuItemId(navigationMenuItem!.id); + navigate(navigationPath); + } + : undefined; const shouldNavigate = !isLayoutCustomizationModeEnabled; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerOpenedSection.tsx index d01bc67b2e..8b1a8dadf8 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerOpenedSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationDrawerOpenedSection.tsx @@ -1,6 +1,4 @@ -import { useParams } from 'react-router-dom'; - -import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems'; +import { useIdentifyActiveNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems'; import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useLingui } from '@lingui/react/macro'; @@ -12,21 +10,14 @@ export const NavigationDrawerOpenedSection = () => { const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); - const { objectMetadataIdsInWorkspaceNav } = useWorkspaceNavigationMenuItems(); - - const { - objectNamePlural: currentObjectNamePlural, - objectNameSingular: currentObjectNameSingular, - } = useParams(); + const { objectMetadataIdForOpenedSection } = + useIdentifyActiveNavigationMenuItems(); const objectMetadataItem = activeObjectMetadataItems.find( - (item) => - item.namePlural === currentObjectNamePlural || - item.nameSingular === currentObjectNameSingular, + (item) => item.id === objectMetadataIdForOpenedSection, ); - const shouldShowOpenedSection = isDefined(objectMetadataItem) - ? !objectMetadataIdsInWorkspaceNav.has(objectMetadataItem.id) - : false; + + const shouldShowOpenedSection = isDefined(objectMetadataItem); return ( diff --git a/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts b/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts index c9cdac315c..5f53835262 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts @@ -1,5 +1,5 @@ import { atom, type WritableAtom } from 'jotai'; -import { atomWithStorage } from 'jotai/utils'; +import { atomWithStorage, createJSONStorage } from 'jotai/utils'; import { isDefined } from 'twenty-shared/utils'; import { type State } from '@/ui/utilities/state/jotai/types/State'; @@ -28,12 +28,14 @@ export const createAtomState = ({ key, defaultValue, useLocalStorage = false, + useSessionStorage = false, localStorageOptions, useCookieStorage, }: { key: string; defaultValue: ValueType; useLocalStorage?: boolean; + useSessionStorage?: boolean; localStorageOptions?: LocalStorageOptions; useCookieStorage?: CookieStorageConfig; }): State => { @@ -51,6 +53,11 @@ export const createAtomState = ({ storage, { getOnInit: true }, ) as StateAtom; + } else if (useSessionStorage) { + const storage = createJSONStorage(() => sessionStorage); + baseAtom = atomWithStorage(key, defaultValue, storage, { + getOnInit: true, + }) as StateAtom; } else if (useLocalStorage) { baseAtom = atomWithStorage( key,