Refactor navigation menu item folder handling

- Introduced new utility functions `doesFolderNavigationMenuItemMatchUrlForSelection` and `resolveFolderSelectedNavigationMenuItemIndex` to streamline the logic for matching folder navigation items with the current URL and determining the selected item index.
- Updated `useNavigationMenuItemFolderOpenState` to utilize the new functions, enhancing clarity and maintainability of the navigation state management.
- Removed deprecated logic related to active navigation item checks, contributing to a cleaner implementation.

These changes improve the overall navigation handling within the folder structure of the menu.
This commit is contained in:
Abdul Rahman
2026-04-09 16:52:45 +05:30
parent 6af013b1dd
commit 65db4d8d9a
3 changed files with 211 additions and 85 deletions
@@ -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,
@@ -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<View, 'id' | 'objectMetadataId'>[];
};
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
);
});
};
@@ -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<View, 'id' | 'objectMetadataId'>[];
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;
};