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:
+9
@@ -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,
|
||||
});
|
||||
-42
@@ -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);
|
||||
});
|
||||
});
|
||||
-15
@@ -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;
|
||||
};
|
||||
+11
-8
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
+8
-8
@@ -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,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
+23
-9
@@ -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}
|
||||
|
||||
+27
-34
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+182
@@ -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 };
|
||||
};
|
||||
-21
@@ -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,
|
||||
};
|
||||
};
|
||||
+22
-22
@@ -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;
|
||||
|
||||
|
||||
+6
-15
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user