Compare commits

...
Author SHA1 Message Date
Abdul Rahman c7bc3542e4 Refactor folder navigation menu item URL matching logic
- Simplified the logic for determining if a folder navigation menu item matches the current URL by consolidating the retrieval of object metadata.
- Removed redundant checks and streamlined the flow for better readability and maintainability.
2026-04-13 19:55:10 +05:30
Abdul Rahman 8e53ba0f4a Merge branch 'main' into fix/sidebar-nav-active-on-record-show 2026-04-13 19:52:23 +05:30
Abdul Rahman 00bad2cb99 Refactor selected navigation menu item index resolution
- Updated the logic in `resolveFolderSelectedNavigationMenuItemIndex` to improve clarity and maintainability by introducing a `selectedIndex` variable.
- Simplified the return logic to ensure the correct index is returned based on the active navigation item checks.
- These changes enhance the overall navigation handling within the folder structure of the menu.
2026-04-09 19:09:28 +05:30
Abdul Rahman 6e05bbdf12 Refactor active navigation item handling and introduce utility functions
- Moved the `ActiveNavigationItem` type definition to a new file for better organization and clarity.
- Updated `activeNavigationItemState` to utilize the new type definition, enhancing type safety.
- Refactored navigation logic in `NavigationDrawerItemForObjectMetadataItem` to use the new `isNavigationItemActive` utility function, simplifying the active state determination.
- Introduced `shouldShowOpenedSection` utility to streamline the logic for determining if a section should be expanded based on the active navigation item.

These changes improve the maintainability and clarity of the navigation handling within the components.
2026-04-09 19:02:25 +05:30
Abdul Rahman 65db4d8d9a 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.
2026-04-09 16:52:45 +05:30
Abdul Rahman 6af013b1dd Refactor workspace navigation menu item handling
- Renamed `objectMetadataIdsInWorkspaceNav` to `objectMetadataIdsWithObjectWorkspaceItem` for improved clarity.
- Simplified filtering logic in `useWorkspaceNavigationMenuItems` to focus solely on OBJECT types.
- Enhanced `NavigationDrawerOpenedSection` to utilize the updated hook and improve the logic for determining when to show the opened section based on active navigation state.

These changes contribute to better readability and maintainability of the navigation logic within the components.
2026-04-09 08:03:33 +05:30
Abdul Rahman 74232f718e Refactor navigation menu item open state logic
- Simplified the logic for determining the first non-link item in the navigation menu, improving readability and maintainability.
- Updated the active navigation item checks to enhance clarity, replacing the previous method with a more straightforward function for matching navigation items to the active object.
- Improved the handling of selected navigation menu item index to reflect the updated logic for active navigation items.

These changes contribute to a clearer and more efficient navigation handling within the component.
2026-04-09 07:11:27 +05:30
Abdul Rahman 6dbec4a7e2 Refactor variable names for clarity in NavigationDrawerItemForObjectMetadataItem
- Updated variable names to improve readability and maintainability, changing `isOnRecordShowPage` to `isOnObjectRecordShowPage` and `isOnIndexPage` to `isOnObjectIndexPage`.
- Enhanced the logic for determining the current path matching object metadata, contributing to clearer navigation handling within the component.
2026-04-09 06:56:42 +05:30
Abdul Rahman 9f55948785 Refactor active navigation item logic in NavigationDrawerItemForObjectMetadataItem
- Enhanced the calculation of active navigation item state by introducing checks for the current path, improving the accuracy of the `isActive` determination.
- Simplified the logic for determining if the current path matches the object metadata, contributing to better maintainability and clarity in the component's navigation handling.
2026-04-09 06:51:53 +05:30
Abdul Rahman ff7cf245c4 Refactor active state logic in NavigationDrawerItemForObjectMetadataItem
- Updated the `isActive` calculation to include a check for `navItemId` when `isAtomForCurrentObject` is true, enhancing the accuracy of active navigation item detection.
- This change improves the clarity and maintainability of the component's navigation logic, aligning with recent refactoring efforts.
2026-04-08 20:38:22 +05:30
Abdul Rahman aae3f4301b Remove unused handleBeforeNavigation call in NavigationDrawerItem component
- Eliminated the `handleBeforeNavigation` function call from the `handleExternalLinkClick` method to streamline the navigation logic.
- This change enhances code clarity by removing unnecessary function calls, contributing to improved maintainability of the component.

These updates simplify the navigation handling within the `NavigationDrawerItem`, aligning with recent refactoring efforts.
2026-04-08 20:35:36 +05:30
Abdul Rahman a3d7ef76cb Enhance workspace navigation menu item logic
- Updated `useWorkspaceNavigationMenuItems` to include support for both OBJECT and VIEW types in the navigation menu.
- Integrated `getObjectMetadataForNavigationMenuItem` to retrieve metadata for navigation items, improving the handling of object and view metadata.
- Utilized `useAtomStateValue` to access `objectMetadataItems` and `views`, enhancing state management within the hook.

These changes improve the flexibility and accuracy of the workspace navigation menu, allowing for better integration of different item types.
2026-04-08 19:39:08 +05:30
Abdul Rahman af66b6acc8 Remove unused navigationState prop from NavigationDrawer components and related hooks
- Eliminated the `navigationState` prop from `NavigationDrawerItem`, `NavigationDrawerSubItem`, and `useMouseDownNavigation` to streamline navigation logic.
- Updated the navigation logic to remove unnecessary state handling, enhancing clarity and maintainability.

These changes simplify the navigation components and improve overall code quality.
2026-04-08 19:28:34 +05:30
Abdul Rahman 25ee116332 Add active navigation item state management and enhance navigation components
- Introduced `activeNavigationItemState` to manage the currently active navigation item across components.
- Updated `NavigationMenuItemFolderSubItem`, `NavigationDrawerItemForObjectMetadataItem`, and related components to utilize the new state for improved navigation tracking.
- Enhanced `onBeforeNavigation` functionality to set the active navigation item before routing, ensuring consistent state management.
- Refactored navigation logic to improve clarity and maintainability.

These changes enhance user experience by providing better tracking of active navigation items and streamlining navigation interactions.
2026-04-08 19:25:23 +05:30
Abdul Rahman 1b2e4899a2 Refactor navigation menu item logic and enhance state management
- Simplified `isLocationMatchingNavigationMenuItem` by removing unnecessary parameters and streamlining the matching logic.
- Updated components to utilize the new `navigationState` prop for better state management across navigation items.
- Enhanced `NavigationDrawerItem` and related components to support active navigation item tracking.
- Improved the `useMouseDownNavigation` hook to pass navigation state during routing.

These changes improve code clarity, maintainability, and user experience in navigation interactions.
2026-04-08 18:55:08 +05:30
Abdul Rahman ba12a26541 Refactor navigation path matching logic
- Updated `isLocationMatchingNavigationMenuItem` to utilize the new `matchesRecordShowPathForObject` utility for improved path matching.
- Introduced `matchesRecordShowPathForObject` to encapsulate the logic for checking if a pathname matches the record show path format.
- Adjusted `NavigationDrawerItemForObjectMetadataItem` to leverage the new matching utility, enhancing code clarity and maintainability.

This refactor streamlines the navigation matching process and promotes code reuse.
2026-04-03 11:26:59 +05:30
Abdul Rahman 918fdcbf87 Enhance navigation menu item matching logic
- Updated `isLocationMatchingNavigationMenuItem` to include additional parameters for better matching of object record show paths.
- Introduced logging for debugging navigation matches, capturing relevant data for analysis.
- Added tests to ensure correct behavior for object navigation items on record show pages, distinguishing between view-only and object pins.
- Refactored related components to utilize the updated matching logic.

This improves the accuracy of navigation item states and enhances debugging capabilities.
2026-04-03 11:05:21 +05:30
15 changed files with 504 additions and 65 deletions
@@ -0,0 +1,8 @@
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const activeNavigationItemState =
createAtomState<ActiveNavigationItem | null>({
key: 'activeNavigationItemState',
defaultValue: null,
});
@@ -0,0 +1,4 @@
export type ActiveNavigationItem = {
navItemId: string;
objectNameSingular: string;
};
@@ -1,5 +1,5 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
import { NavigationMenuItemType } from 'twenty-shared/types';
describe('isLocationMatchingNavigationMenuItem', () => {
it('should return true when item link matches current path (non-view) or current view path (view)', () => {
@@ -0,0 +1,14 @@
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
export const matchesRecordShowPathForObject = (
pathname: string,
objectNameSingular: string,
): boolean => {
const recordShowBase = getAppPath(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: '',
});
return pathname.startsWith(`${recordShowBase}/`);
};
@@ -3,6 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
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';
@@ -16,6 +17,7 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
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 = {
@@ -79,6 +81,17 @@ export const NavigationMenuItemFolderSubItem = ({
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem));
const setActiveNavigationItem = useSetAtomState(activeNavigationItemState);
const handleBeforeNavigation = () => {
if (isDefined(objectNameSingular)) {
setActiveNavigationItem({
navItemId: navigationMenuItem.id,
objectNameSingular,
});
}
};
const handleClick =
onClick ??
(isEditable
@@ -108,6 +121,7 @@ export const NavigationMenuItemFolderSubItem = ({
objectMetadataItem ?? undefined,
)}
to={isDragging || handleClick ? undefined : computedLink}
onBeforeNavigation={handleBeforeNavigation}
onClick={handleClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={isEditHighlightedInNavigationMenu}
@@ -1,15 +1,16 @@
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 { isNonEmptyString } from '@sniptt/guards';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
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 { 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';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
@@ -31,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);
@@ -54,45 +56,35 @@ export const useNavigationMenuItemFolderOpenState = ({
}
if (!isOpen) {
const firstNonLinkItem = navigationMenuItems.find((item) => {
for (const item of navigationMenuItems) {
if (item.type === NavigationMenuItemType.LINK) {
return false;
continue;
}
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isNonEmptyString(computedLink);
});
if (isDefined(firstNonLinkItem)) {
const link = getNavigationMenuItemComputedLink(
firstNonLinkItem,
item,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
break;
}
}
}
};
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
(item) => {
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
item.type,
computedLink,
);
},
);
const activeNavigationItem = useAtomStateValue(activeNavigationItemState);
const selectedNavigationMenuItemIndex =
resolveFolderSelectedNavigationMenuItemIndex({
navigationMenuItems,
activeNavigationItem,
currentPath,
currentViewPath,
objectMetadataItems,
views,
workspaceNavigationMenuItems,
});
return {
isOpen,
@@ -0,0 +1,101 @@
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,
);
const matchesByLink = isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
folderChildNavigationMenuItem.type,
computedLink,
);
const objectMetadataForFolderChild = getObjectMetadataForNavigationMenuItem(
folderChildNavigationMenuItem,
objectMetadataItems,
views,
);
const matchesByRecordShow =
!matchesByLink &&
isDefined(objectMetadataForFolderChild) &&
matchesRecordShowPathForObject(
currentPath,
objectMetadataForFolderChild.nameSingular,
);
if (!matchesByLink && !matchesByRecordShow) {
return false;
}
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,94 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
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 ResolveFolderSelectedNavigationMenuItemIndexParams = {
navigationMenuItems: NavigationMenuItem[];
activeNavigationItem: ActiveNavigationItem | 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,
}),
);
let selectedIndex: number;
if (!isActiveNavigationItemObjectInFolder) {
selectedIndex = urlMatchIndex;
} else if (explicitMatchIndex !== -1) {
selectedIndex = explicitMatchIndex;
} else if (recordMatchIndex !== -1) {
selectedIndex = recordMatchIndex;
} else {
selectedIndex = urlMatchIndex;
}
return selectedIndex;
};
@@ -1,21 +1,34 @@
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
export const useWorkspaceNavigationMenuItems = (): {
objectMetadataIdsInWorkspaceNav: Set<string>;
objectMetadataIdsWithObjectWorkspaceItem: Set<string>;
} => {
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const objectMetadataIdsInWorkspaceNav = new Set(
const objectMetadataIdsWithObjectWorkspaceItem = new Set(
rawWorkspaceNavigationMenuItems
.filter((item) => item.type === NavigationMenuItemType.OBJECT)
.map((item) => item.targetObjectMetadataId)
.map(
(item) =>
getObjectMetadataForNavigationMenuItem(
item,
objectMetadataItems,
views,
)?.id,
)
.filter((objectMetadataId) => isDefined(objectMetadataId)),
);
return {
objectMetadataIdsInWorkspaceNav,
objectMetadataIdsWithObjectWorkspaceItem,
};
};
@@ -3,7 +3,9 @@ import { isNonEmptyString } from '@sniptt/guards';
import { Fragment, type ReactNode, useContext } from 'react';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier';
import { isNavigationItemActive } from '@/navigation-menu-item/display/object/utils/isNavigationItemActive';
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,6 +17,7 @@ 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 {
@@ -68,8 +71,6 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const { getIcon } = useIcons();
const objectNavItemColor = getObjectColorWithFallback(objectMetadataItem);
const location = useLocation();
const currentPath = location.pathname;
const currentPathWithSearch = `${location.pathname}${location.search}`;
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
@@ -90,19 +91,26 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
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 activeNavigationItem = useAtomStateValue(activeNavigationItemState);
const setActiveNavigationItem = useSetAtomState(activeNavigationItemState);
const isActive = isNavigationItemActive({
navigationMenuItem,
activeNavigationItem,
objectNameSingular: objectMetadataItem.nameSingular,
objectNamePlural: objectMetadataItem.namePlural,
computedLink,
currentLocation: location,
});
const handleBeforeNavigation = () => {
if (isDefined(navigationMenuItem?.id)) {
setActiveNavigationItem({
navItemId: navigationMenuItem.id,
objectNameSingular: objectMetadataItem.nameSingular,
});
}
};
const handleClick = isLayoutCustomizationModeEnabled
? onEditModeClick
@@ -184,6 +192,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
? navigationPath
: undefined
}
onBeforeNavigation={handleBeforeNavigation}
onClick={handleClick}
Icon={Icon}
iconColor={iconThemeColor}
@@ -0,0 +1,78 @@
import { AppPath, NavigationMenuItemType } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
type IsNavigationItemActiveParams = {
navigationMenuItem: Pick<NavigationMenuItem, 'id' | 'type'> | undefined;
activeNavigationItem: ActiveNavigationItem | null;
objectNameSingular: string;
objectNamePlural: string;
computedLink: string;
currentLocation: { pathname: string; search: string };
};
export const isNavigationItemActive = ({
navigationMenuItem,
activeNavigationItem,
objectNameSingular,
objectNamePlural,
computedLink,
currentLocation,
}: IsNavigationItemActiveParams): boolean => {
const currentPath = currentLocation.pathname;
const currentPathWithSearch = `${currentLocation.pathname}${currentLocation.search}`;
const navigationMenuItemType = navigationMenuItem?.type;
const isRecord = navigationMenuItemType === NavigationMenuItemType.RECORD;
const isObject = navigationMenuItemType === NavigationMenuItemType.OBJECT;
const hasCustomLink =
isRecord ||
isObject ||
navigationMenuItemType === NavigationMenuItemType.VIEW;
const isOnObjectRecordShowPage = matchesRecordShowPathForObject(
currentPath,
objectNameSingular,
);
const isOnObjectIndexPage =
currentPath === getAppPath(AppPath.RecordIndexPage, { objectNamePlural });
const isCurrentPathMatchingObject =
isOnObjectIndexPage || isOnObjectRecordShowPage;
const matchesNavigationMenuItemLink =
hasCustomLink &&
isDefined(navigationMenuItemType) &&
isLocationMatchingNavigationMenuItem(
currentPath,
currentPathWithSearch,
navigationMenuItemType,
computedLink,
);
const isActiveByUrl =
matchesNavigationMenuItemLink ||
(isObject && isOnObjectRecordShowPage) ||
(!hasCustomLink && isCurrentPathMatchingObject);
const shouldUseExplicitActiveItem =
isDefined(activeNavigationItem) &&
activeNavigationItem.objectNameSingular === objectNameSingular &&
isCurrentPathMatchingObject;
if (!shouldUseExplicitActiveItem || !isDefined(navigationMenuItem)) {
return isActiveByUrl;
}
const isRecordMatchingCurrentPage = isRecord && computedLink === currentPath;
return (
activeNavigationItem.navItemId === navigationMenuItem.id ||
isRecordMatchingCurrentPage
);
};
@@ -1,18 +1,27 @@
import { useParams } from 'react-router-dom';
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useLingui } from '@lingui/react/macro';
import { useLocation, useParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { activeNavigationItemState } from '@/navigation-menu-item/common/states/activeNavigationItemState';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { shouldShowOpenedSection } from '@/navigation-menu-item/display/sections/utils/shouldShowOpenedSection';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
export const NavigationDrawerOpenedSection = () => {
const { t } = useLingui();
const { pathname } = useLocation();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const { objectMetadataIdsInWorkspaceNav } = useWorkspaceNavigationMenuItems();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
const activeNavigationItem = useAtomStateValue(activeNavigationItemState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const {
objectNamePlural: currentObjectNamePlural,
@@ -24,12 +33,18 @@ export const NavigationDrawerOpenedSection = () => {
item.namePlural === currentObjectNamePlural ||
item.nameSingular === currentObjectNameSingular,
);
const shouldShowOpenedSection = isDefined(objectMetadataItem)
? !objectMetadataIdsInWorkspaceNav.has(objectMetadataItem.id)
: false;
const isExpanded = shouldShowOpenedSection({
objectMetadataItem,
pathname,
activeNavigationItem,
workspaceNavigationMenuItems,
objectMetadataItems,
views,
});
return (
<AnimatedExpandableContainer isExpanded={shouldShowOpenedSection}>
<AnimatedExpandableContainer isExpanded={isExpanded}>
<NavigationDrawerSectionForObjectMetadataItems
sectionTitle={t`Opened`}
objectMetadataItems={
@@ -0,0 +1,93 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ActiveNavigationItem } from '@/navigation-menu-item/common/types/ActiveNavigationItem';
import { matchesRecordShowPathForObject } from '@/navigation-menu-item/common/utils/matchesRecordShowPathForObject';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type View } from '@/views/types/View';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
type ShouldShowOpenedSectionParams = {
objectMetadataItem: EnrichedObjectMetadataItem | undefined;
pathname: string;
activeNavigationItem: ActiveNavigationItem | null;
workspaceNavigationMenuItems: NavigationMenuItem[];
objectMetadataItems: EnrichedObjectMetadataItem[];
views: Pick<View, 'id' | 'objectMetadataId'>[];
};
export const shouldShowOpenedSection = ({
objectMetadataItem,
pathname,
activeNavigationItem,
workspaceNavigationMenuItems,
objectMetadataItems,
views,
}: ShouldShowOpenedSectionParams): boolean => {
if (!isDefined(objectMetadataItem)) {
return false;
}
if (
!matchesRecordShowPathForObject(pathname, objectMetadataItem.nameSingular)
) {
return false;
}
const hasTopLevelObjectWorkspaceItem = workspaceNavigationMenuItems.some(
(item) => {
if (item.type !== NavigationMenuItemType.OBJECT) {
return false;
}
const metadata = getObjectMetadataForNavigationMenuItem(
item,
objectMetadataItems,
views,
);
return isDefined(metadata) && metadata.id === objectMetadataItem.id;
},
);
if (hasTopLevelObjectWorkspaceItem) {
return false;
}
if (!isDefined(activeNavigationItem)) {
return true;
}
if (
activeNavigationItem.objectNameSingular !== objectMetadataItem.nameSingular
) {
return true;
}
const activeWorkspaceNavigationMenuItem = workspaceNavigationMenuItems.find(
(item) => item.id === activeNavigationItem.navItemId,
);
if (!isDefined(activeWorkspaceNavigationMenuItem)) {
return true;
}
const isViewOrRecord =
activeWorkspaceNavigationMenuItem.type === NavigationMenuItemType.VIEW ||
activeWorkspaceNavigationMenuItem.type === NavigationMenuItemType.RECORD;
if (!isViewOrRecord) {
return true;
}
const objectMetadataForActiveItem = getObjectMetadataForNavigationMenuItem(
activeWorkspaceNavigationMenuItem,
objectMetadataItems,
views,
);
const targetsCurrentObject =
isDefined(objectMetadataForActiveItem) &&
objectMetadataForActiveItem.id === objectMetadataItem.id;
return !targetsCurrentObject;
};
@@ -62,6 +62,7 @@ export type NavigationDrawerItemProps = {
preventCollapseOnMobile?: boolean;
isSelectedInEditMode?: boolean;
variant?: 'default' | 'tertiary';
onBeforeNavigation?: () => void;
};
type StyledItemProps = Pick<
@@ -267,6 +268,7 @@ export const NavigationDrawerItem = ({
preventCollapseOnMobile = false,
isSelectedInEditMode = false,
variant = 'default',
onBeforeNavigation: onBeforeNavigationProp,
}: NavigationDrawerItemProps) => {
const { theme } = useContext(ThemeContext);
const isMobile = useIsMobile();
@@ -286,10 +288,11 @@ export const NavigationDrawerItem = ({
const showBreadcrumb = indentationLevel === 2;
const showStyledSpacer = isDefined(modifier) || isDefined(rightOptions);
const handleMobileNavigation = () => {
const handleBeforeNavigation = () => {
if (isMobile && !preventCollapseOnMobile) {
setIsNavigationDrawerExpanded(false);
}
onBeforeNavigationProp?.();
};
const isExternalLink =
@@ -297,7 +300,6 @@ export const NavigationDrawerItem = ({
const isInternalLink = isDefined(to) && !isExternalLink;
const handleExternalLinkClick = () => {
handleMobileNavigation();
if (isDefined(to)) {
window.open(to, '_blank', 'noopener,noreferrer');
}
@@ -309,7 +311,7 @@ export const NavigationDrawerItem = ({
} = useMouseDownNavigation({
to: isExternalLink ? undefined : to,
onClick: isExternalLink ? (onClick ?? handleExternalLinkClick) : onClick,
onBeforeNavigation: handleMobileNavigation,
onBeforeNavigation: handleBeforeNavigation,
triggerEvent,
});
@@ -21,6 +21,7 @@ export const NavigationDrawerSubItem = ({
isSelectedInEditMode,
triggerEvent,
variant,
onBeforeNavigation,
}: NavigationDrawerSubItemProps) => {
return (
<NavigationDrawerItem
@@ -40,6 +41,7 @@ export const NavigationDrawerSubItem = ({
isSelectedInEditMode={isSelectedInEditMode}
triggerEvent={triggerEvent}
variant={variant}
onBeforeNavigation={onBeforeNavigation}
/>
);
};