Fix active navigation item disambiguation (#19664)

## 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<number>` instead of a single index

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2026-04-15 18:30:47 +05:30
committed by GitHub
parent 7956ecc7b2
commit 762fb6fd64
12 changed files with 296 additions and 175 deletions
@@ -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,
});
@@ -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);
});
});
@@ -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;
};
@@ -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 (
<NavigationMenuItemFolderLayout
@@ -129,7 +133,7 @@ const NavigationMenuItemFolderReadOnlyContent = ({
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
active={!isOpen && selectedNavigationMenuItemIndex >= 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}
/>
))}
@@ -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,
})}
/>
)}
@@ -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 (
<NavigationDrawerSubItem
@@ -107,14 +121,14 @@ export const NavigationMenuItemFolderSubItem = ({
navigationMenuItem,
objectMetadataItem ?? undefined,
)}
to={isDragging || handleClick ? undefined : computedLink}
to={isDragging || isEditable ? undefined : computedLink}
onClick={handleClick}
active={index === selectedNavigationMenuItemIndex}
active={isActive}
isSelectedInEditMode={isEditHighlightedInNavigationMenu}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength,
selectedIndex: selectedNavigationMenuItemIndex,
selectedIndex: isActive ? index : -1,
})}
rightOptions={rightOptions}
isDragging={isDragging}
@@ -1,13 +1,14 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useLocation, useNavigate } from 'react-router-dom';
import { 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 { currentNavigationMenuItemFolderIdState } from '@/navigation-menu-item/common/states/currentNavigationMenuItemFolderIdState';
import { lastClickedNavigationMenuItemIdState } from '@/navigation-menu-item/common/states/lastClickedNavigationMenuItemIdState';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
import { useIdentifyActiveNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useIdentifyActiveNavigationMenuItems';
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';
@@ -17,17 +18,14 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type UseNavigationMenuItemFolderOpenStateParams = {
folderId: string;
navigationMenuItems: NavigationMenuItem[];
folderChildrenNavigationMenuItems: NavigationMenuItem[];
};
export const useNavigationMenuItemFolderOpenState = ({
folderId,
navigationMenuItems,
folderChildrenNavigationMenuItems,
}: UseNavigationMenuItemFolderOpenStateParams) => {
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,
};
};
@@ -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 };
};
@@ -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<string>;
} => {
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,
};
};
@@ -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;
@@ -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 (
<AnimatedExpandableContainer isExpanded={shouldShowOpenedSection}>
@@ -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 = <ValueType>({
key,
defaultValue,
useLocalStorage = false,
useSessionStorage = false,
localStorageOptions,
useCookieStorage,
}: {
key: string;
defaultValue: ValueType;
useLocalStorage?: boolean;
useSessionStorage?: boolean;
localStorageOptions?: LocalStorageOptions;
useCookieStorage?: CookieStorageConfig<ValueType>;
}): State<ValueType> => {
@@ -51,6 +53,11 @@ export const createAtomState = <ValueType>({
storage,
{ getOnInit: true },
) as StateAtom<ValueType>;
} else if (useSessionStorage) {
const storage = createJSONStorage<ValueType>(() => sessionStorage);
baseAtom = atomWithStorage<ValueType>(key, defaultValue, storage, {
getOnInit: true,
}) as StateAtom<ValueType>;
} else if (useLocalStorage) {
baseAtom = atomWithStorage<ValueType>(
key,