refactor: remove ProcessedNavigationMenuItem, derive display fields at point of use (#18687)

## Summary

- Removes `ProcessedNavigationMenuItem` type and the
`sortNavigationMenuItems` enrichment pipeline that pre-computed display
fields (label, link, icon, avatarUrl, etc.) for every navigation menu
item upfront
- Replaces with `filterAndSortNavigationMenuItems` (pure filter+sort
returning raw `NavigationMenuItem[]`) and small utility functions
(`getNavigationMenuItemLabel`, `getNavigationMenuItemComputedLink`,
`getNavigationMenuItemObjectNameSingular`) that components call on
demand
- Eliminates the redundant `itemType` field (was identical to the raw
`type` field) across ~30 consumer files
- Each type-specific renderer
(`NavigationDrawerItemForObjectMetadataItem`, `NavigationMenuItemIcon`,
link/folder components) now derives only the 1-2 display fields it
actually needs from the raw item + globally available Jotai atoms

Net result: -1199 / +1177 lines, 7 files deleted, 4 new utility files.
This commit is contained in:
Charles Bochet
2026-03-17 00:33:06 +01:00
committed by GitHub
parent 48285be164
commit 1bb642d7cd
55 changed files with 1188 additions and 1227 deletions
@@ -1,21 +1,29 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { useLocation } from 'react-router-dom';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import {
AppPath,
CoreObjectNameSingular,
NavigationMenuItemType,
} from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export type NavigationDrawerItemForObjectMetadataItemProps = {
objectMetadataItem: ObjectMetadataItem;
navigationMenuItem?: ProcessedNavigationMenuItem;
navigationMenuItem?: NavigationMenuItem;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
onActiveItemClickWhenNotInEditMode?: () => void;
@@ -36,6 +44,8 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewId =
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
@@ -48,24 +58,28 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const currentPath = location.pathname;
const currentPathWithSearch = `${location.pathname}${location.search}`;
const isRecord =
navigationMenuItem?.itemType === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.itemType === NavigationMenuItemType.VIEW;
const isObject =
navigationMenuItem?.itemType === NavigationMenuItemType.OBJECT;
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
const isObject = navigationMenuItem?.type === NavigationMenuItemType.OBJECT;
const hasCustomLink = isRecord || isView || isObject;
const navigationPath = hasCustomLink
? navigationMenuItem!.link
? getNavigationMenuItemComputedLink(
navigationMenuItem!,
objectMetadataItems,
views,
)
: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
lastVisitedViewId ? { viewId: lastVisitedViewId } : undefined,
);
const computedLink = hasCustomLink ? navigationPath : '';
const isActive = hasCustomLink
? (isView || isObject ? currentPathWithSearch : currentPath) ===
navigationMenuItem!.link
computedLink
: currentPath ===
getAppPath(AppPath.RecordIndexPage, {
objectNamePlural: objectMetadataItem.namePlural,
@@ -81,17 +95,32 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const shouldNavigate = !isNavigationMenuInEditMode;
const view = isDefined(navigationMenuItem?.viewId)
? views.find((view) => view.id === navigationMenuItem!.viewId)
: undefined;
const viewKey = view?.key ?? null;
const isViewWithCustomName =
isView &&
navigationMenuItem?.viewKey !== ViewKey.INDEX &&
isDefined(navigationMenuItem?.labelIdentifier);
isView && viewKey !== ViewKey.INDEX && isDefined(view);
const itemLabel = isDefined(navigationMenuItem)
? getNavigationMenuItemLabel(navigationMenuItem, objectMetadataItems, views)
: objectMetadataItem.labelPlural;
const label = isRecord
? navigationMenuItem!.labelIdentifier
? itemLabel
: isViewWithCustomName
? navigationMenuItem!.labelIdentifier
? itemLabel
: objectMetadataItem.labelPlural;
const recordIdentifier =
isRecord && isDefined(navigationMenuItem?.targetRecordIdentifier)
? recordIdentifierToObjectRecordIdentifier({
recordIdentifier: navigationMenuItem!.targetRecordIdentifier!,
objectMetadataItem,
})
: null;
const Icon = isRecord
? () => (
<Avatar
@@ -100,16 +129,16 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
? 'squared'
: 'rounded'
}
avatarUrl={navigationMenuItem!.avatarUrl}
avatarUrl={recordIdentifier?.avatarUrl}
placeholderColorSeed={navigationMenuItem!.targetRecordId ?? undefined}
placeholder={navigationMenuItem!.labelIdentifier}
placeholder={itemLabel}
/>
)
: isViewWithCustomName && isDefined(navigationMenuItem?.Icon)
: isViewWithCustomName && isDefined(view?.icon)
? () => (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(navigationMenuItem!.Icon!)}
ViewIcon={getIcon(view!.icon)}
objectColor={objectNavItemColor}
/>
)
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
@@ -18,7 +18,7 @@ export const NavigationDrawerSectionForWorkspaceItemContent = ({
onActiveObjectMetadataItemClick,
readOnly,
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
switch (item.itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
return (
<NavigationDrawerSectionForWorkspaceItemFolderContent
@@ -24,7 +24,7 @@ export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({
}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => {
const folderId = item.id;
const folderName = item.name ?? 'Folder';
const folderIconKey = item.Icon;
const folderIconKey = item.icon;
const folderColor = 'color' in item ? item.color : undefined;
const navigationMenuItems = folderChildrenById.get(item.id) ?? [];
const isGroup = folderCount > 1;
@@ -1,9 +1,12 @@
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { IconArrowUpRight } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -18,15 +21,24 @@ export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const linkItem = item as ProcessedNavigationMenuItem;
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const label = getNavigationMenuItemLabel(item, objectMetadataItems, views);
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return (
<NavigationDrawerItem
label={linkItem.labelIdentifier}
to={isNavigationMenuInEditMode || isDragging ? undefined : linkItem.link}
label={label}
to={isNavigationMenuInEditMode || isDragging ? undefined : computedLink}
onClick={
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
active={false}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
isDragging={isDragging}
@@ -1,5 +1,4 @@
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -18,7 +17,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
);
@@ -28,7 +27,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
return (
<NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem}
navigationMenuItem={item as ProcessedNavigationMenuItem}
navigationMenuItem={item}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
isDragging={isDragging}
@@ -1,17 +1,14 @@
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import React, { lazy, Suspense, useContext } from 'react';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import {
type FlatWorkspaceItem,
type NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback';
@@ -40,7 +37,7 @@ const StyledWorkspaceSectionContentGapOffset = styled.div`
type NavigationDrawerSectionForWorkspaceItemsProps = {
sectionTitle: string;
items: FlatWorkspaceItem[];
items: NavigationMenuItem[];
rightIcon?: React.ReactNode;
selectedNavigationMenuItemId?: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
@@ -75,33 +72,34 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
const isAddToNavigationDropTargetVisible =
addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS;
const folderChildrenById = items.reduce<
Map<string, ProcessedNavigationMenuItem[]>
>((acc, item) => {
const folderId = item.folderId;
if (isDefined(folderId)) {
const children = acc.get(folderId) ?? [];
children.push(item as ProcessedNavigationMenuItem);
acc.set(folderId, children);
}
return acc;
}, new Map());
const folderChildrenById = items.reduce<Map<string, NavigationMenuItem[]>>(
(acc, item) => {
const folderId = item.folderId;
if (isDefined(folderId)) {
const children = acc.get(folderId) ?? [];
children.push(item);
acc.set(folderId, children);
}
return acc;
},
new Map(),
);
const filteredItems = flatItems.filter((item) => {
const type = item.itemType;
const itemType = item.type;
if (
type === NavigationMenuItemType.FOLDER ||
type === NavigationMenuItemType.LINK
itemType === NavigationMenuItemType.FOLDER ||
itemType === NavigationMenuItemType.LINK
) {
return true;
}
if (
type === NavigationMenuItemType.OBJECT ||
type === NavigationMenuItemType.VIEW ||
type === NavigationMenuItemType.RECORD
itemType === NavigationMenuItemType.OBJECT ||
itemType === NavigationMenuItemType.VIEW ||
itemType === NavigationMenuItemType.RECORD
) {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
);
@@ -116,17 +114,19 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
return false;
});
const getEditModeProps = (item: FlatWorkspaceItem): EditModeProps => {
const getEditModeProps = (item: NavigationMenuItem): EditModeProps => {
const itemId = item.id;
return {
isSelectedInEditMode: selectedNavigationMenuItemId === itemId,
onEditModeClick: onNavigationMenuItemClick
? () => {
const type = item.itemType;
const itemType = item.type;
const objectMetadataItem =
type === 'OBJECT' || type === 'VIEW' || type === 'RECORD'
itemType === 'OBJECT' ||
itemType === 'VIEW' ||
itemType === 'RECORD'
? getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
)
@@ -1,7 +1,7 @@
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
@@ -46,7 +46,7 @@ export const WorkspaceSectionListDndKit = ({
NavigationDropTargetContext,
);
const folderCount = filteredItems.filter(
(item) => item.itemType === NavigationMenuItemType.FOLDER,
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
return (
@@ -1,8 +1,8 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import type { FlatWorkspaceItem } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
@@ -29,12 +29,12 @@ export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
const folderCount = filteredItems.filter(
(item) => item.itemType === NavigationMenuItemType.FOLDER,
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
return (
<StyledList>
{filteredItems.map((item: FlatWorkspaceItem) => (
{filteredItems.map((item: NavigationMenuItem) => (
<NavigationDrawerSectionForWorkspaceItemContent
key={item.id}
item={item}
@@ -5,12 +5,12 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
@@ -35,7 +35,7 @@ type WorkspaceFolderReadOnlyProps = {
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
isGroup: boolean;
};
@@ -1,17 +1,14 @@
import type {
FlatWorkspaceItem,
NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionItemContentProps = {
item: FlatWorkspaceItem;
item: NavigationMenuItem;
editModeProps: EditModeProps;
isDragging: boolean;
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
folderChildrenById: Map<string, NavigationMenuItem[]>;
folderCount: number;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
@@ -1,16 +1,13 @@
import type {
FlatWorkspaceItem,
NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionListDndKitProps = {
filteredItems: FlatWorkspaceItem[];
getEditModeProps: (item: FlatWorkspaceItem) => EditModeProps;
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
filteredItems: NavigationMenuItem[];
getEditModeProps: (item: NavigationMenuItem) => EditModeProps;
folderChildrenById: Map<string, NavigationMenuItem[]>;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (