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
@@ -5,7 +5,7 @@ import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
@@ -86,7 +86,7 @@ export const AddToNavigationDragHandle = ({
? payload.iconColor
: undefined;
const effectiveColor = getEffectiveNavigationMenuItemColor(
{ itemType: payload.type as NavigationMenuItemType },
{ type: payload.type as NavigationMenuItemType },
objectColor,
);
const hasBackgroundColor =
@@ -1,11 +1,14 @@
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { useContext, useState } from 'react';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
@@ -18,16 +21,17 @@ import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigati
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useRenameNavigationMenuItemFolder';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
@@ -45,14 +49,15 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
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';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
type CurrentWorkspaceMemberNavigationMenuItemsProps = {
folder: {
id: string;
folderName: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
isGroup: boolean;
isWorkspaceFolder?: boolean;
@@ -65,6 +70,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
const { t } = useLingui();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
@@ -105,13 +111,26 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
}
if (!isOpen) {
const firstNonLinkItem = folder.navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
const firstNonLinkItem = folder.navigationMenuItems.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,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
}
};
@@ -131,8 +150,19 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
const { closeDropdown } = useCloseDropdown();
const selectedNavigationMenuItemIndex = folder.navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
(item) => {
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
item.type,
computedLink,
);
},
);
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
@@ -261,53 +291,84 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{folder.navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
to={isDragging ? undefined : navigationMenuItem.link}
active={index === selectedNavigationMenuItemIndex}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
rightOptions={
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
{folder.navigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find(
(view) => view.id === navigationMenuItem.viewId,
)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})
}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)
}
isDragging={isDragging}
triggerEvent="CLICK"
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
/>
}
/>
))}
)}
to={isDragging ? undefined : computedLink}
active={index === selectedNavigationMenuItemIndex}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
rightOptions={
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(
navigationMenuItem.id,
);
}}
accent="tertiary"
/>
)
}
isDragging={isDragging}
triggerEvent="CLICK"
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
/>
}
/>
);
})}
{provided.placeholder}
</div>
)}
@@ -14,10 +14,14 @@ import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/N
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
@@ -31,6 +35,7 @@ const StyledOrphanNavigationMenuItemsContainer = styled.div`
export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
const currentPath = useLocation().pathname;
@@ -48,58 +53,77 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
>
{orphanNavigationMenuItems.length > 0 ? (
<>
{orphanNavigationMenuItems.map((navigationMenuItem, index) => (
<NavigationItemDropTarget
key={navigationMenuItem.id}
folderId={null}
index={index}
sectionId={NavigationSections.FAVORITES}
>
<DraggableItem
draggableId={navigationMenuItem.id}
{orphanNavigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<NavigationItemDropTarget
key={navigationMenuItem.id}
folderId={null}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem,
)}
to={isDragging ? undefined : navigationMenuItem.link}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
</StyledOrphanNavigationMenuItemsContainer>
}
/>
</NavigationItemDropTarget>
))}
sectionId={NavigationSections.FAVORITES}
>
<DraggableItem
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem.type,
computedLink,
)}
to={isDragging ? undefined : computedLink}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
</StyledOrphanNavigationMenuItemsContainer>
}
/>
</NavigationItemDropTarget>
);
})}
<NavigationItemDropTarget
folderId={null}
index={orphanNavigationMenuItems.length}
@@ -1,21 +1,25 @@
import { Droppable } from '@hello-pangea/dnd';
import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/components/NavigationMenuItemBackButton';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type NavigationMenuItemFolderContentProps = {
folderId: string;
folderName: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
export const NavigationMenuItemFolderContent = ({
@@ -24,6 +28,7 @@ export const NavigationMenuItemFolderContent = ({
navigationMenuItems,
}: NavigationMenuItemFolderContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
return (
@@ -36,41 +41,60 @@ export const NavigationMenuItemFolderContent = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={navigationMenuItem.link}
/>
}
/>
))}
{navigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={computedLink}
/>
}
/>
);
})}
{provided.placeholder}
</div>
)}
@@ -1,12 +1,12 @@
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemFolderContent } from '@/navigation-menu-item/components/NavigationMenuItemFolderContent';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
type NavigationMenuItemFolderContentDispatcherEffectProps = {
folderName: string;
folderId: string;
navigationMenuItems?: ProcessedNavigationMenuItem[];
navigationMenuItems?: NavigationMenuItem[];
};
export const NavigationMenuItemFolderContentDispatcherEffect = ({
@@ -1,68 +1,86 @@
import { isNonEmptyString } from '@sniptt/guards';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
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 { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
export const NavigationMenuItemIcon = ({
navigationMenuItem,
}: {
navigationMenuItem: ProcessedNavigationMenuItem;
navigationMenuItem: NavigationMenuItem;
}) => {
const { getIcon } = useIcons();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
navigationMenuItem.objectNameSingular ?? '',
);
const views = useAtomStateValue(viewsSelector);
const isRecord =
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
) ?? '';
const { Icon: StandardIcon, IconColor } =
useGetStandardObjectIcon(objectNameSingular);
const isRecord = navigationMenuItem.type === NavigationMenuItemType.RECORD;
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isViewWithOverlay =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.viewKey !== ViewKey.INDEX;
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
isDefined(view) &&
view.key !== ViewKey.INDEX;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
);
const objectNavItemColor = useObjectNavItemColor(
navigationMenuItem.objectNameSingular ?? '',
(item) => item.nameSingular === objectNameSingular,
);
const objectNavItemColor = useObjectNavItemColor(objectNameSingular);
const objectIconForView =
objectMetadataItem?.icon != null
? getIcon(objectMetadataItem.icon)
: StandardIcon;
const canShowViewOverlay =
isViewWithOverlay &&
isDefined(objectIconForView) &&
isDefined(navigationMenuItem.Icon);
isViewWithOverlay && isDefined(objectIconForView) && isDefined(view?.icon);
if (canShowViewOverlay) {
return (
<ObjectIconWithViewOverlay
ObjectIcon={objectIconForView}
ViewIcon={getIcon(navigationMenuItem.Icon!)}
ViewIcon={getIcon(view!.icon)}
objectColor={objectNavItemColor}
/>
);
}
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<LinkIconWithLinkOverlay
link={navigationMenuItem.link}
link={computedLink}
LinkIcon={IconLink}
DefaultIcon={IconWorld}
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
@@ -70,9 +88,13 @@ export const NavigationMenuItemIcon = ({
);
}
const iconToUse =
StandardIcon ??
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
const itemIcon = isRecord
? undefined
: objectMetadataItem?.icon
? getIcon(objectMetadataItem.icon)
: undefined;
const iconToUse = StandardIcon ?? itemIcon;
const effectiveColor = getEffectiveNavigationMenuItemColor(
navigationMenuItem,
objectNavItemColor,
@@ -88,14 +110,30 @@ export const NavigationMenuItemIcon = ({
? IconColor
: themeCssVariables.font.color.secondary;
const labelIdentifier = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const recordIdentifier =
isRecord &&
isDefined(navigationMenuItem.targetRecordIdentifier) &&
isDefined(objectMetadataItem)
? recordIdentifierToObjectRecordIdentifier({
recordIdentifier: navigationMenuItem.targetRecordIdentifier,
objectMetadataItem,
})
: null;
const avatar = (
<Avatar
size={iconStyle ? 'sm' : 'md'}
type={navigationMenuItem.avatarType}
type={recordIdentifier?.avatarType ?? 'icon'}
Icon={iconToUse}
iconColor={iconColorToUse}
avatarUrl={navigationMenuItem.avatarUrl}
placeholder={navigationMenuItem.labelIdentifier}
avatarUrl={recordIdentifier?.avatarUrl ?? ''}
placeholder={labelIdentifier}
placeholderColorSeed={navigationMenuItem.targetRecordId ?? undefined}
/>
);
@@ -6,21 +6,24 @@ import {
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
draggableProvided: DraggableProvided;
draggableSnapshot: DraggableStateSnapshot;
rubric: DraggableRubric;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
navigationMenuItemFolderContentLength: number;
selectedNavigationMenuItemIndex: number;
};
@@ -35,12 +38,28 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
const { theme } = useContext(ThemeContext);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const navigationMenuItem = navigationMenuItems[rubric.source.index];
if (!isDefined(navigationMenuItem)) {
return null;
}
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<div
ref={draggableProvided.innerRef}
@@ -57,15 +76,14 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
>
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.INDEX
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
})
}
label={navigationMenuItem.labelIdentifier}
label={label}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
@@ -1,12 +1,15 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
@@ -15,7 +18,7 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderSubItemProps = {
navigationMenuItem: ProcessedNavigationMenuItem;
navigationMenuItem: NavigationMenuItem;
index: number;
arrayLength: number;
selectedNavigationMenuItemIndex: number;
@@ -38,20 +41,22 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const objectMetadataItem =
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT ||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
navigationMenuItem.type === NavigationMenuItemType.VIEW ||
navigationMenuItem.type === NavigationMenuItemType.RECORD
? getObjectMetadataForNavigationMenuItem(
navigationMenuItem,
objectMetadataItems,
views,
)
: null;
const isEditableInEditMode =
isNavigationMenuInEditMode &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.itemType === NavigationMenuItemType.LINK ||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem));
const handleEditModeClick =
@@ -63,26 +68,42 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
})
: undefined;
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.INDEX
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
})
}
label={navigationMenuItem.labelIdentifier}
label={label}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
to={isContextDragging || handleEditModeClick ? undefined : computedLink}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
@@ -13,7 +13,7 @@ import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import {
@@ -25,16 +25,19 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
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';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useStore } from 'jotai';
import { SidePanelPages } from 'twenty-shared/types';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -45,6 +48,8 @@ const StyledRightIconsContainer = styled.div`
export const WorkspaceNavigationMenuItems = () => {
const items = useWorkspaceSectionItems();
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const store = useStore();
const enterEditMode = () => {
const currentNavigationMenuItems = store.get(
@@ -88,16 +93,27 @@ export const WorkspaceNavigationMenuItems = () => {
);
openNavigationMenuItemInSidePanel({
pageTitle: t`Edit folder`,
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
pageIcon: getIcon(item.icon ?? FOLDER_ICON_DEFAULT),
});
const firstChild = workspaceNavigationMenuItemsSorted.find(
(navItem) =>
navItem.folderId === folderId &&
navItem.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(navItem.link),
);
if (firstChild?.link) {
navigate(firstChild.link);
const firstChild = workspaceNavigationMenuItemsSorted.find((navItem) => {
if (navItem.folderId !== folderId) return false;
if (navItem.type === NavigationMenuItemType.LINK) return false;
const link = getNavigationMenuItemComputedLink(
navItem,
objectMetadataItems,
views,
);
return isNonEmptyString(link);
});
if (firstChild) {
const link = getNavigationMenuItemComputedLink(
firstChild,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
};
@@ -106,16 +122,21 @@ export const WorkspaceNavigationMenuItems = () => {
objectMetadataItem: ObjectMetadataItem | null | undefined,
) => {
if (objectMetadataItem) {
const label =
item.type === NavigationMenuItemType.VIEW ||
item.type === NavigationMenuItemType.OBJECT
? getNavigationMenuItemLabel(item, objectMetadataItems, views)
: objectMetadataItem.labelSingular;
openNavigationMenuItemInSidePanel({
pageTitle:
item.itemType === NavigationMenuItemType.VIEW ||
item.itemType === NavigationMenuItemType.OBJECT
? item.labelIdentifier
: objectMetadataItem.labelSingular,
pageTitle: label,
pageIcon: getIcon(objectMetadataItem.icon),
});
}
const link = 'link' in item ? item.link : undefined;
const link = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
@@ -127,7 +148,7 @@ export const WorkspaceNavigationMenuItems = () => {
const { item, objectMetadataItem } = params;
setSelectedNavigationMenuItemInEditMode(item.id);
switch (item.itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
openFolderAndNavigateToFirstChild(item.id, item);
break;
@@ -36,8 +36,8 @@ import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/So
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
@@ -76,7 +76,7 @@ type WorkspaceNavigationMenuItemsFolderProps = {
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
isGroup: boolean;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
@@ -1,7 +0,0 @@
export enum NavigationMenuItemType {
FOLDER = 'FOLDER',
LINK = 'LINK',
OBJECT = 'OBJECT',
RECORD = 'RECORD',
VIEW = 'VIEW',
}
@@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
@@ -1,11 +1,9 @@
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -16,7 +14,7 @@ type NavigationMenuItemFolder = {
folderName: string;
icon?: string | null;
color?: string | null;
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
navigationMenuItems: NavigationMenuItem[];
};
type NavigationMenuItemFolderEntry = Pick<
@@ -85,43 +83,10 @@ export const useNavigationMenuItemsByFolder = () => {
return sortedFolders.reduce<NavigationMenuItemFolder[]>((acc, folder) => {
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
const targetRecordIdentifiersMap = itemsInFolder.reduce<
Map<string, ObjectRecordIdentifier>
>((map, item) => {
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
return map;
}
const targetRecordIdentifier = item.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return map;
}
const itemObjectMetadata = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (isDefined(itemObjectMetadata)) {
const objectRecordIdentifier =
recordIdentifierToObjectRecordIdentifier({
recordIdentifier: targetRecordIdentifier,
objectMetadataItem: itemObjectMetadata,
});
map.set(itemTargetRecordId, objectRecordIdentifier);
}
return map;
}, new Map());
const sortedItems = sortNavigationMenuItems(
const sortedItems = filterAndSortNavigationMenuItems(
itemsInFolder,
true,
views,
objectMetadataItems,
targetRecordIdentifiersMap,
);
acc.push({
@@ -1,20 +1,31 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
const getLabelForItemType = (
itemType: NavigationMenuItemType,
item: { name?: string | null; labelIdentifier?: string | null },
const getLabelForItem = (
item: NavigationMenuItem,
objectMetadataItems: Parameters<typeof getNavigationMenuItemLabel>[1],
views: Parameters<typeof getNavigationMenuItemLabel>[2],
objectLabelSingular?: string | null,
): string => {
switch (itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
return item.name ?? 'Folder';
case NavigationMenuItemType.LINK:
return item.name ?? 'Link';
case NavigationMenuItemType.OBJECT:
case NavigationMenuItemType.VIEW:
return item.labelIdentifier ?? objectLabelSingular ?? '';
return (
getNavigationMenuItemLabel(item, objectMetadataItems, views) ||
objectLabelSingular ||
''
);
default:
return objectLabelSingular ?? '';
}
@@ -24,11 +35,14 @@ export const useSelectedNavigationMenuItemEditItemLabel = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const selectedItemLabel = selectedItem
? getLabelForItemType(
selectedItem.itemType,
? getLabelForItem(
selectedItem,
objectMetadataItems,
views,
selectedItemObjectMetadata?.labelSingular,
)
: null;
@@ -1,13 +1,7 @@
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
import { isNavigationMenuItemObject } from '@/navigation-menu-item/utils/isNavigationMenuItemObject';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -19,99 +13,21 @@ export const useSortedNavigationMenuItems = () => {
const views = useAtomStateValue(viewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const targetRecordIdentifiers = useMemo(() => {
const identifiersMap = new Map<string, ObjectRecordIdentifier>();
[...navigationMenuItems, ...workspaceNavigationMenuItems].forEach(
(navigationMenuItem) => {
if (isDefined(navigationMenuItem.viewId)) {
return;
}
const itemTargetRecordId = navigationMenuItem.targetRecordId;
if (!isDefined(itemTargetRecordId)) {
return;
}
const targetRecordIdentifier =
navigationMenuItem.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return;
}
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier(
{
recordIdentifier: targetRecordIdentifier,
objectMetadataItem,
},
);
identifiersMap.set(itemTargetRecordId, objectRecordIdentifier);
},
);
return identifiersMap;
}, [navigationMenuItems, workspaceNavigationMenuItems, objectMetadataItems]);
const navigationMenuItemsSorted = useMemo(() => {
return sortNavigationMenuItems(
return filterAndSortNavigationMenuItems(
navigationMenuItems,
true,
views,
objectMetadataItems,
targetRecordIdentifiers,
);
}, [
navigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
]);
}, [navigationMenuItems, views, objectMetadataItems]);
const workspaceNavigationMenuItemsSorted = useMemo(() => {
const filtered = workspaceNavigationMenuItems.filter((item) => {
if (isNavigationMenuItemFolder(item)) {
return true;
}
if (isNavigationMenuItemLink(item)) {
return true;
}
if (isNavigationMenuItemObject(item)) {
return isDefined(item.targetObjectMetadataId);
}
if (isDefined(item.viewId)) {
return views.some((view) => view.id === item.viewId);
}
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId)) {
return false;
}
const matchesTargetRecord =
targetRecordIdentifiers.has(itemTargetRecordId);
return matchesTargetRecord;
});
return sortNavigationMenuItems(
filtered,
true,
return filterAndSortNavigationMenuItems(
workspaceNavigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
);
}, [
workspaceNavigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
]);
}, [workspaceNavigationMenuItems, views, objectMetadataItems]);
return {
navigationMenuItemsSorted,
@@ -1,19 +1,23 @@
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 { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
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';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type UseWorkspaceFolderOpenStateParams = {
folderId: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
export const useWorkspaceFolderOpenState = ({
@@ -25,6 +29,8 @@ export const useWorkspaceFolderOpenState = ({
const currentPath = location.pathname;
const currentViewPath = location.pathname + location.search;
const isMobile = useIsMobile();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsState);
@@ -48,20 +54,44 @@ export const useWorkspaceFolderOpenState = ({
}
if (!isOpen) {
const firstNonLinkItem = navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
const firstNonLinkItem = navigationMenuItems.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,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
}
};
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
(item) => {
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
item.type,
computedLink,
);
},
);
return {
@@ -1,8 +1,7 @@
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
@@ -17,19 +16,12 @@ import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
export type FlatWorkspaceItem =
| ProcessedNavigationMenuItem
| (NavigationMenuItem & {
itemType: NavigationMenuItemType.FOLDER;
Icon: string;
});
export type NavigationMenuItemClickParams = {
item: FlatWorkspaceItem;
item: NavigationMenuItem;
objectMetadataItem?: ObjectMetadataItem | null;
};
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
export const useWorkspaceSectionItems = (): NavigationMenuItem[] => {
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { workspaceNavigationMenuItemsByFolder } =
@@ -42,7 +34,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
.filter((item) => !isDefined(item.folderId))
.sort((a, b) => a.position - b.position);
const processedObjectViewsById = new Map(
const processedItemsById = new Map(
workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]),
);
@@ -53,25 +45,24 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
]),
);
const flatItems: FlatWorkspaceItem[] = flatWorkspaceItems.reduce<
FlatWorkspaceItem[]
const flatItems: NavigationMenuItem[] = flatWorkspaceItems.reduce<
NavigationMenuItem[]
>((acc, item) => {
if (isNavigationMenuItemFolder(item)) {
acc.push({
...item,
itemType: NavigationMenuItemType.FOLDER,
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
icon: item.icon ?? FOLDER_ICON_DEFAULT,
});
} else {
const processedItem = processedObjectViewsById.get(item.id);
if (!isDefined(processedItem)) {
const validItem = processedItemsById.get(item.id);
if (!isDefined(validItem)) {
return acc;
}
if (processedItem.itemType === NavigationMenuItemType.LINK) {
acc.push(processedItem);
if (validItem.type === NavigationMenuItemType.LINK) {
acc.push(validItem);
} else {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
processedItem,
validItem,
objectMetadataItems,
views,
);
@@ -82,7 +73,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
objectMetadataItem.id,
).canReadObjectRecords
) {
acc.push(processedItem);
acc.push(validItem);
}
}
}
@@ -90,7 +81,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
}, []);
return flatItems.flatMap((item) =>
item.itemType === NavigationMenuItemType.FOLDER
item.type === NavigationMenuItemType.FOLDER
? [item, ...(folderChildrenById.get(item.id) ?? [])]
: [item],
);
@@ -1,11 +0,0 @@
import { type ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { type NavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
import { type NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
export type ProcessedNavigationMenuItem = NavigationMenuItem &
NavigationMenuItemDisplayFields & {
viewKey?: ViewKey | null;
itemType: NavigationMenuItemType;
};
@@ -1,60 +0,0 @@
import { computeNavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
describe('computeNavigationMenuItemDisplayFields', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
} as ObjectMetadataItem;
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
id: 'record-id',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
linkToShowPage: '/app/objects/people/record-id',
};
it('should return null when objectMetadataItem or objectRecordIdentifier is null', () => {
expect(
computeNavigationMenuItemDisplayFields(null, mockObjectRecordIdentifier),
).toBeNull();
expect(
computeNavigationMenuItemDisplayFields(mockObjectMetadataItem, null),
).toBeNull();
});
it('should return display fields from metadata and record identifier', () => {
const result = computeNavigationMenuItemDisplayFields(
mockObjectMetadataItem,
mockObjectRecordIdentifier,
);
expect(result).toEqual({
labelIdentifier: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
link: '/app/objects/people/record-id',
objectNameSingular: 'person',
});
});
it('should default optional identifier fields to empty string or icon', () => {
const minimal: ObjectRecordIdentifier = {
id: 'record-id',
name: 'Jane Doe',
};
const result = computeNavigationMenuItemDisplayFields(
mockObjectMetadataItem,
minimal,
);
expect(result?.avatarUrl).toBe('');
expect(result?.avatarType).toBe('icon');
expect(result?.link).toBe('');
});
});
@@ -0,0 +1,242 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
describe('filterAndSortNavigationMenuItems', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelPlural: 'People',
icon: 'IconUser',
} as ObjectMetadataItem;
const mockView: Pick<View, 'id' | 'objectMetadataId' | 'key'> = {
id: 'view-id',
objectMetadataId: 'metadata-id',
key: ViewKey.INDEX,
};
it('should return empty array when navigationMenuItems is empty', () => {
const result = filterAndSortNavigationMenuItems([], [], []);
expect(result).toEqual([]);
});
it('should keep view items when view and objectMetadata exist', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.VIEW,
viewId: 'view-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[mockView],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('item-id');
});
it('should filter out view items when view is not found', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.VIEW,
viewId: 'non-existent-view-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should keep record items when targetRecordIdentifier exists', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id',
labelIdentifier: 'John Doe',
},
position: 2,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('item-id');
});
it('should filter out record items when targetRecordId is not defined', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should filter out record items when objectMetadataItem is not found', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id',
targetObjectMetadataId: 'non-existent-metadata-id',
targetRecordIdentifier: {
id: 'record-id',
labelIdentifier: 'John',
},
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should sort navigation menu items by position', () => {
const navigationMenuItems = [
{
id: 'item-3',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-3',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-3',
labelIdentifier: 'C',
},
position: 3,
},
{
id: 'item-1',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-1',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-1',
labelIdentifier: 'A',
},
position: 1,
},
{
id: 'item-2',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-2',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-2',
labelIdentifier: 'B',
},
position: 2,
},
] as NavigationMenuItem[];
const result = filterAndSortNavigationMenuItems(
navigationMenuItems,
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('item-1');
expect(result[1].id).toBe('item-2');
expect(result[2].id).toBe('item-3');
});
it('should keep link items', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'link-1',
type: NavigationMenuItemType.LINK,
link: 'https://example.com',
name: 'My Link',
position: 1,
} as NavigationMenuItem,
],
[],
[],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('link-1');
});
it('should keep object items when objectMetadata exists', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'obj-1',
type: NavigationMenuItemType.OBJECT,
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem,
],
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('obj-1');
});
it('should filter out object items when objectMetadata is not found', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'obj-1',
type: NavigationMenuItemType.OBJECT,
targetObjectMetadataId: 'non-existent-id',
position: 1,
} as NavigationMenuItem,
],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should keep folder items', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'folder-1',
type: NavigationMenuItemType.FOLDER,
name: 'My Folder',
position: 1,
} as NavigationMenuItem,
],
[],
[],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('folder-1');
});
});
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
@@ -30,7 +30,7 @@ const mockViews: View[] = [
describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for link item type', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.LINK },
{ type: NavigationMenuItemType.LINK },
mockObjectMetadataItems,
mockViews,
);
@@ -39,7 +39,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return object metadata for view item when view and metadata exist', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'view-1' },
{ type: NavigationMenuItemType.VIEW, viewId: 'view-1' },
mockObjectMetadataItems,
mockViews,
);
@@ -49,7 +49,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for view item when view is not found', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
{ type: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
mockObjectMetadataItems,
mockViews,
);
@@ -61,7 +61,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
{ id: 'orphan-view', objectMetadataId: 'non-existent-metadata' } as View,
];
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
{ type: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
mockObjectMetadataItems,
viewsWithOrphanView,
);
@@ -71,7 +71,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return object metadata for record item when metadata exists', () => {
const result = getObjectMetadataForNavigationMenuItem(
{
itemType: NavigationMenuItemType.RECORD,
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'metadata-2',
},
mockObjectMetadataItems,
@@ -83,7 +83,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for record item when targetObjectMetadataId is undefined', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.RECORD },
{ type: NavigationMenuItemType.RECORD },
mockObjectMetadataItems,
mockViews,
);
@@ -93,7 +93,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for record item when metadata is not found', () => {
const result = getObjectMetadataForNavigationMenuItem(
{
itemType: NavigationMenuItemType.RECORD,
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'non-existent-metadata',
},
mockObjectMetadataItems,
@@ -104,7 +104,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for view item when viewId is undefined', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW },
{ type: NavigationMenuItemType.VIEW },
mockObjectMetadataItems,
mockViews,
);
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
describe('isLocationMatchingNavigationMenuItem', () => {
@@ -7,20 +7,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
isLocationMatchingNavigationMenuItem(
'/app/objects/people',
'/app/objects/people?viewId=123',
{
itemType: NavigationMenuItemType.RECORD,
link: '/app/objects/people',
},
NavigationMenuItemType.RECORD,
'/app/objects/people',
),
).toBe(true);
expect(
isLocationMatchingNavigationMenuItem(
'/app/objects/companies',
'/app/objects/companies?viewId=123',
{
itemType: NavigationMenuItemType.VIEW,
link: '/app/objects/companies?viewId=123',
},
NavigationMenuItemType.VIEW,
'/app/objects/companies?viewId=123',
),
).toBe(true);
});
@@ -30,20 +26,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
isLocationMatchingNavigationMenuItem(
'/app/objects/people',
'/app/objects/people?viewId=123',
{
itemType: NavigationMenuItemType.RECORD,
link: '/app/objects/company',
},
NavigationMenuItemType.RECORD,
'/app/objects/company',
),
).toBe(false);
expect(
isLocationMatchingNavigationMenuItem(
'/app/objects/companies',
'/app/objects/companies?viewId=123',
{
itemType: NavigationMenuItemType.VIEW,
link: '/app/objects/companies?viewId=456',
},
NavigationMenuItemType.VIEW,
'/app/objects/companies?viewId=456',
),
).toBe(false);
});
@@ -1,360 +0,0 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
jest.mock('twenty-shared/utils', () => {
const actual = jest.requireActual('twenty-shared/utils');
return {
...actual,
getAppPath: jest.fn((path, params, query) => {
const basePath = `/app/objects/${params.objectNamePlural}`;
const viewId = query?.viewId;
if (viewId !== undefined && viewId !== null) {
return `${basePath}?viewId=${viewId}`;
}
return basePath;
}),
};
});
describe('sortNavigationMenuItems', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelPlural: 'People',
icon: 'IconUser',
} as ObjectMetadataItem;
const mockView: Pick<
View,
'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'
> = {
id: 'view-id',
name: 'All People',
objectMetadataId: 'metadata-id',
icon: 'IconUser',
key: ViewKey.INDEX,
};
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
id: 'record-id',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
linkToShowPage: '/app/objects/people/record-id',
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return empty array when navigationMenuItems is empty', () => {
const result = sortNavigationMenuItems([], true, [], [], new Map());
expect(result).toEqual([]);
});
it('should process view link navigation menu items', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'view-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[mockView],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: 'item-id',
viewId: 'view-id',
position: 1,
labelIdentifier: 'People',
objectNameSingular: 'person',
Icon: 'IconUser',
});
expect(result[0].link).toContain('viewId=view-id');
});
it('should return null for view link when view is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'non-existent-view-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should process record link navigation menu items', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
labelIdentifier: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
link: '/app/objects/people/record-id',
objectNameSingular: 'person',
});
});
it('should return null for record link when targetRecordId is not defined', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should return null for record link when objectMetadataItem is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'non-existent-metadata-id',
position: 1,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toEqual([]);
});
it('should return null for record link when targetRecordIdentifier is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'non-existent-record-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should return empty link when hasLinkToShowPage is false', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
false,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(1);
expect(result[0].link).toBe('');
});
it('should sort navigation menu items by position', () => {
const navigationMenuItems: NavigationMenuItem[] = [
{
id: 'item-3',
targetRecordId: 'record-id-3',
targetObjectMetadataId: 'metadata-id',
position: 3,
},
{
id: 'item-1',
targetRecordId: 'record-id-1',
targetObjectMetadataId: 'metadata-id',
position: 1,
},
{
id: 'item-2',
targetRecordId: 'record-id-2',
targetObjectMetadataId: 'metadata-id',
position: 2,
},
] as NavigationMenuItem[];
const targetRecordIdentifiers = new Map([
['record-id-1', { ...mockObjectRecordIdentifier, id: 'record-id-1' }],
['record-id-2', { ...mockObjectRecordIdentifier, id: 'record-id-2' }],
['record-id-3', { ...mockObjectRecordIdentifier, id: 'record-id-3' }],
]);
const result = sortNavigationMenuItems(
navigationMenuItems,
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('item-1');
expect(result[1].id).toBe('item-2');
expect(result[2].id).toBe('item-3');
});
it('should handle mixed view and record link items', () => {
const navigationMenuItems: NavigationMenuItem[] = [
{
id: 'view-item',
viewId: 'view-id',
position: 1,
},
{
id: 'record-item',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
},
] as NavigationMenuItem[];
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
navigationMenuItems,
true,
[mockView],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('view-item');
expect(result[0].objectNameSingular).toBe('person');
expect(result[1].id).toBe('record-item');
expect(result[1].objectNameSingular).toBe('person');
});
it('should handle navigationMenuItem with both viewId and targetRecordId (viewId takes precedence)', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'view-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[mockView],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toHaveLength(1);
expect(result[0].objectNameSingular).toBe('person');
expect(result[0].viewId).toBe('view-id');
});
it('should process link items with protocol normalization and label from name or link', () => {
const withProtocol = sortNavigationMenuItems(
[
{
id: 'link-1',
type: NavigationMenuItemType.LINK,
link: 'https://example.com',
name: 'My Link',
position: 1,
} as NavigationMenuItem,
],
true,
[],
[],
new Map(),
);
expect(withProtocol[0].labelIdentifier).toBe('My Link');
expect(withProtocol[0].link).toBe('https://example.com');
const noProtocol = sortNavigationMenuItems(
[
{
id: 'link-2',
type: NavigationMenuItemType.LINK,
link: 'example.com',
position: 2,
} as NavigationMenuItem,
],
true,
[],
[],
new Map(),
);
expect(noProtocol[0].link).toBe('https://example.com');
expect(noProtocol[0].labelIdentifier).toBe('example.com');
});
});
@@ -1,31 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { isDefined } from 'twenty-shared/utils';
export type NavigationMenuItemDisplayFields = {
labelIdentifier: string;
avatarUrl: string;
avatarType: 'icon' | 'rounded' | 'squared';
link: string;
objectNameSingular: string;
Icon?: string;
};
export const computeNavigationMenuItemDisplayFields = (
objectMetadataItem: ObjectMetadataItem | null,
objectRecordIdentifier: ObjectRecordIdentifier | null,
): NavigationMenuItemDisplayFields | null => {
if (!isDefined(objectMetadataItem) || !isDefined(objectRecordIdentifier)) {
return null;
}
const objectNameSingular = objectMetadataItem.nameSingular;
return {
labelIdentifier: objectRecordIdentifier.name,
avatarUrl: objectRecordIdentifier.avatarUrl ?? '',
avatarType: objectRecordIdentifier.avatarType ?? 'icon',
link: objectRecordIdentifier.linkToShowPage ?? '',
objectNameSingular,
};
};
@@ -0,0 +1,51 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const filterAndSortNavigationMenuItems = (
navigationMenuItems: NavigationMenuItem[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
objectMetadataItems: Pick<ObjectMetadataItem, 'id'>[],
): NavigationMenuItem[] => {
return navigationMenuItems
.filter((item) => {
if (item.type === NavigationMenuItemType.FOLDER) {
return true;
}
if (item.type === NavigationMenuItemType.LINK) {
return true;
}
if (item.type === NavigationMenuItemType.OBJECT) {
return (
isDefined(item.targetObjectMetadataId) &&
objectMetadataItems.some(
(meta) => meta.id === item.targetObjectMetadataId,
)
);
}
if (item.type === NavigationMenuItemType.VIEW) {
if (!isDefined(item.viewId)) {
return false;
}
const view = views.find((view) => view.id === item.viewId);
return (
isDefined(view) &&
objectMetadataItems.some((meta) => meta.id === view.objectMetadataId)
);
}
if (item.type === NavigationMenuItemType.RECORD) {
return (
isDefined(item.targetRecordId) &&
isDefined(item.targetObjectMetadataId) &&
isDefined(item.targetRecordIdentifier) &&
objectMetadataItems.some(
(meta) => meta.id === item.targetObjectMetadataId,
)
);
}
return false;
})
.sort((a, b) => a.position - b.position);
};
@@ -2,27 +2,27 @@ import { isNonEmptyString } from '@sniptt/guards';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
export const getEffectiveNavigationMenuItemColor = (
navigationMenuItem: {
itemType: NavigationMenuItemType;
type: NavigationMenuItemType;
color?: string | null;
},
objectColor?: string,
): string | undefined => {
if (navigationMenuItem.itemType === NavigationMenuItemType.FOLDER) {
if (navigationMenuItem.type === NavigationMenuItemType.FOLDER) {
return isNonEmptyString(navigationMenuItem.color)
? navigationMenuItem.color
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.OBJECT) {
if (navigationMenuItem.type === NavigationMenuItemType.OBJECT) {
return objectColor;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.VIEW) {
if (navigationMenuItem.type === NavigationMenuItemType.VIEW) {
return objectColor;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
}
return undefined;
@@ -0,0 +1,76 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { AppPath, NavigationMenuItemType } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemComputedLink = (
item: NavigationMenuItem,
objectMetadataItems: ObjectMetadataItem[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
): string => {
switch (item.type) {
case NavigationMenuItemType.OBJECT: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return '';
}
const indexView = views.find(
(view) =>
view.objectMetadataId === objectMetadataItem.id &&
view.key === ViewKey.INDEX,
);
return getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
indexView ? { viewId: indexView.id } : {},
);
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return '';
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return '';
}
return getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
{ viewId: item.viewId! },
);
}
case NavigationMenuItemType.LINK: {
const linkUrl = (item.link ?? '').trim();
if (linkUrl.startsWith('http://') || linkUrl.startsWith('https://')) {
return linkUrl;
}
return linkUrl ? `https://${linkUrl}` : '';
}
case NavigationMenuItemType.RECORD: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (
!isDefined(objectMetadataItem) ||
!isDefined(item.targetRecordIdentifier)
) {
return '';
}
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier({
recordIdentifier: item.targetRecordIdentifier,
objectMetadataItem,
});
return objectRecordIdentifier.linkToShowPage ?? '';
}
default:
return '';
}
};
@@ -0,0 +1,49 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemLabel = (
item: NavigationMenuItem,
objectMetadataItems: Pick<
ObjectMetadataItem,
'id' | 'labelPlural' | 'nameSingular'
>[],
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'key'>[],
): string => {
switch (item.type) {
case NavigationMenuItemType.OBJECT: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
return objectMetadataItem?.labelPlural ?? '';
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return '';
}
if (view.key === ViewKey.INDEX) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
return objectMetadataItem?.labelPlural ?? view.name;
}
return view.name;
}
case NavigationMenuItemType.LINK: {
const linkUrl = (item.link ?? '').trim();
return (item.name ?? linkUrl) || 'Link';
}
case NavigationMenuItemType.RECORD: {
return item.targetRecordIdentifier?.labelIdentifier ?? '';
}
case NavigationMenuItemType.FOLDER: {
return item.name ?? 'Folder';
}
default:
return item.name ?? '';
}
};
@@ -0,0 +1,33 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemObjectNameSingular = (
item: NavigationMenuItem,
objectMetadataItems: Pick<ObjectMetadataItem, 'id' | 'nameSingular'>[],
views: Pick<View, 'id' | 'objectMetadataId'>[],
): string | undefined => {
switch (item.type) {
case NavigationMenuItemType.OBJECT:
case NavigationMenuItemType.RECORD: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
return objectMetadataItem?.nameSingular;
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return undefined;
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
return objectMetadataItem?.nameSingular;
}
default:
return undefined;
}
};
@@ -1,56 +1,56 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
type NavigationMenuItemWithItemType = Pick<
ProcessedNavigationMenuItem,
'itemType' | 'viewId' | 'targetObjectMetadataId'
>;
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getObjectMetadataForNavigationMenuItem = (
navigationMenuItem: NavigationMenuItemWithItemType,
navigationMenuItem: Pick<
NavigationMenuItem,
'type' | 'viewId' | 'targetObjectMetadataId'
>,
objectMetadataItems: ObjectMetadataItem[],
views: View[],
views: Pick<View, 'id' | 'objectMetadataId'>[],
): ObjectMetadataItem | null => {
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
return null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT &&
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
return (
objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
) ?? null
);
return objectMetadataItem ?? null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
isDefined(navigationMenuItem.viewId)
) {
const view = views.find((view) => view.id === navigationMenuItem.viewId);
if (!isDefined(view)) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
return (
objectMetadataItems.find((meta) => meta.id === view.objectMetadataId) ??
null
);
return objectMetadataItem ?? null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.RECORD &&
navigationMenuItem.type === NavigationMenuItemType.RECORD &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
return (
objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
) ?? null
);
return objectMetadataItem ?? null;
}
return null;
@@ -1,15 +1,15 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationMenuItemType } from 'twenty-shared/types';
export const isLocationMatchingNavigationMenuItem = (
currentPath: string,
currentViewPath: string,
navigationMenuItem: Pick<ProcessedNavigationMenuItem, 'itemType' | 'link'>,
navigationMenuItemType: NavigationMenuItemType,
computedLink: string,
) => {
const isViewBasedItem =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT;
navigationMenuItemType === NavigationMenuItemType.VIEW ||
navigationMenuItemType === NavigationMenuItemType.OBJECT;
return isViewBasedItem
? navigationMenuItem.link === currentViewPath
: navigationMenuItem.link === currentPath;
? computedLink === currentViewPath
: computedLink === currentPath;
};
@@ -1,179 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import {
computeNavigationMenuItemDisplayFields,
type NavigationMenuItemDisplayFields,
} from './computeNavigationMenuItemDisplayFields';
import { isNavigationMenuItemLink } from './isNavigationMenuItemLink';
export { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
export type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
export const sortNavigationMenuItems = (
navigationMenuItems: NavigationMenuItem[],
hasLinkToShowPage: boolean,
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'>[],
objectMetadataItems: ObjectMetadataItem[],
targetRecordIdentifiers: Map<string, ObjectRecordIdentifier>,
): ProcessedNavigationMenuItem[] => {
return navigationMenuItems
.map((navigationMenuItem) => {
if (
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const indexView = views.find(
(view) =>
view.objectMetadataId === objectMetadataItem.id &&
view.key === ViewKey.INDEX,
);
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier: objectMetadataItem.labelPlural,
avatarUrl: '',
avatarType: 'icon',
link: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
indexView ? { viewId: indexView.id } : {},
),
objectNameSingular: objectMetadataItem.nameSingular,
Icon: objectMetadataItem.icon ?? 'IconBox',
};
return {
...navigationMenuItem,
...displayFields,
viewKey: ViewKey.INDEX,
itemType: NavigationMenuItemType.OBJECT,
};
}
if (isDefined(navigationMenuItem.viewId)) {
const view = views.find(
(view) => view.id === navigationMenuItem.viewId,
);
if (isDefined(view)) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const namePlural = objectMetadataItem.namePlural;
const isIndexView = view.key === ViewKey.INDEX;
const labelIdentifier =
isIndexView && isDefined(objectMetadataItem)
? objectMetadataItem.labelPlural
: view.name;
const icon =
isIndexView &&
isDefined(objectMetadataItem) &&
isDefined(objectMetadataItem.icon)
? objectMetadataItem.icon
: view.icon;
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier,
avatarUrl: '',
avatarType: 'icon',
link: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: namePlural },
{ viewId: navigationMenuItem.viewId },
),
objectNameSingular: objectMetadataItem?.nameSingular ?? 'view',
Icon: icon,
};
return {
...navigationMenuItem,
...displayFields,
viewKey: view.key,
itemType: NavigationMenuItemType.VIEW,
};
}
return null;
}
if (isNavigationMenuItemLink(navigationMenuItem)) {
const linkUrl = (navigationMenuItem.link ?? '').trim();
const normalizedLink =
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
? linkUrl
: `https://${linkUrl}`;
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier: (navigationMenuItem.name ?? linkUrl) || 'Link',
avatarUrl: '',
avatarType: 'icon',
link: normalizedLink,
objectNameSingular: 'link',
Icon: 'IconLink',
};
return {
...navigationMenuItem,
...displayFields,
itemType: NavigationMenuItemType.LINK,
};
}
if (!isDefined(navigationMenuItem.targetRecordId)) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const objectRecordIdentifier = targetRecordIdentifiers.get(
navigationMenuItem.targetRecordId,
);
if (!isDefined(objectRecordIdentifier)) {
return null;
}
const displayFields = computeNavigationMenuItemDisplayFields(
objectMetadataItem,
objectRecordIdentifier,
);
if (!isDefined(displayFields)) {
return null;
}
return {
...navigationMenuItem,
...displayFields,
link: hasLinkToShowPage ? displayFields.link : '',
itemType: NavigationMenuItemType.RECORD,
};
})
.filter(isDefined)
.sort((a, b) => a.position - b.position);
};
@@ -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?: (
@@ -7,7 +7,7 @@ import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoSta
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
@@ -41,7 +41,7 @@ export const SidePanelFolderInfo = () => {
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.FOLDER &&
item.type === NavigationMenuItemType.FOLDER &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
@@ -2,7 +2,7 @@ import { useLingui } from '@lingui/react/macro';
import { IconLink, IconWorld } from 'twenty-ui/display';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
@@ -33,7 +33,7 @@ export const SidePanelLinkInfo = () => {
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.LINK &&
item.type === NavigationMenuItemType.LINK &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
@@ -3,7 +3,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
@@ -15,12 +15,12 @@ export const SidePanelObjectViewRecordInfo = () => {
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const processedItem =
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
const navItem =
selectedItem && selectedItem.type !== NavigationMenuItemType.FOLDER
? selectedItem
: undefined;
if (!processedItem || !selectedItemLabel) {
if (!navItem || !selectedItemLabel) {
return null;
}
@@ -28,22 +28,22 @@ export const SidePanelObjectViewRecordInfo = () => {
NavigationMenuItemType.OBJECT,
NavigationMenuItemType.VIEW,
NavigationMenuItemType.RECORD,
].includes(processedItem.itemType);
].includes(navItem.type);
if (!isObjectViewOrRecord) {
return null;
}
const label =
processedItem.itemType === NavigationMenuItemType.RECORD
navItem.type === NavigationMenuItemType.RECORD
? selectedItemObjectMetadata?.labelSingular
: processedItem.itemType === NavigationMenuItemType.OBJECT
: navItem.type === NavigationMenuItemType.OBJECT
? t`Object`
: t`View`;
return (
<SidePanelPageInfoLayout
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
icon={<NavigationMenuItemIcon navigationMenuItem={navItem} />}
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
label={label}
/>
@@ -5,7 +5,7 @@ import {
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { SidePanelAskAIInfo } from '@/side-panel/components/SidePanelAskAIInfo';
@@ -18,7 +18,6 @@ import { SidePanelPageLayoutInfo } from '@/side-panel/components/SidePanelPageLa
import { SidePanelRecordInfo } from '@/side-panel/components/SidePanelRecordInfo';
import { SidePanelWorkflowStepInfo } from '@/side-panel/components/SidePanelWorkflowStepInfo';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SidePanelPages } from 'twenty-shared/types';
import { type SidePanelContextChipProps } from '@/side-panel/components/SidePanelContextChip';
import { useContext } from 'react';
@@ -51,20 +50,20 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
: undefined;
if (isNavigationMenuItemEditPage && isDefined(selectedNavItem)) {
const itemType = selectedNavItem.itemType;
const navItemType = selectedNavItem.type;
if (itemType === NavigationMenuItemType.FOLDER) {
if (navItemType === NavigationMenuItemType.FOLDER) {
return <SidePanelFolderInfo />;
}
if (itemType === NavigationMenuItemType.LINK) {
if (navItemType === NavigationMenuItemType.LINK) {
return <SidePanelLinkInfo />;
}
if (
itemType === NavigationMenuItemType.OBJECT ||
itemType === NavigationMenuItemType.VIEW ||
itemType === NavigationMenuItemType.RECORD
navItemType === NavigationMenuItemType.OBJECT ||
navItemType === NavigationMenuItemType.VIEW ||
navItemType === NavigationMenuItemType.RECORD
) {
return <SidePanelObjectViewRecordInfo />;
}
@@ -2,8 +2,8 @@ import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
@@ -16,7 +16,7 @@ import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigati
import { TextInput } from '@/ui/input/components/TextInput';
type SidePanelEditLinkItemViewProps = OrganizeActionsProps & {
selectedItem: ProcessedNavigationMenuItem;
selectedItem: NavigationMenuItem;
onUpdateLink: (
linkId: string,
updates: { link?: string; name?: string },
@@ -78,7 +78,7 @@ export const SidePanelEditLinkItemView = ({
<TextInput
fullWidth
placeholder="www.google.com"
value={urlEditInput || selectedItem.link}
value={urlEditInput || selectedItem.link || ''}
onChange={handleUrlChange}
onBlur={handleUrlBlur}
/>
@@ -1,8 +1,9 @@
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
@@ -12,14 +13,16 @@ import {
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
type SidePanelEditObjectViewBaseProps = OrganizeActionsProps & {
onOpenFolderPicker: () => void;
showColorOption?: boolean;
selectedItem?: ProcessedNavigationMenuItem | null;
selectedItem?: NavigationMenuItem | null;
};
export const SidePanelEditObjectViewBase = ({
@@ -36,9 +39,18 @@ export const SidePanelEditObjectViewBase = ({
}: SidePanelEditObjectViewBaseProps) => {
const { t } = useLingui();
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
const objectColor = useObjectNavItemColor(
selectedItem?.objectNameSingular ?? '',
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const views = useAtomStateValue(viewsSelector);
const objectNameSingular = isDefined(selectedItem)
? (getNavigationMenuItemObjectNameSingular(
selectedItem,
objectMetadataItems,
views,
) ?? '')
: '';
const objectColor = useObjectNavItemColor(objectNameSingular);
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
const persistedNavItem = navigationMenuItems.find(
@@ -1,12 +1,11 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
@@ -46,7 +45,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
);
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectedItemType = selectedItem?.itemType ?? null;
const selectedItemType = selectedItem?.type ?? null;
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const openFolderPicker = () =>
@@ -67,10 +66,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const handleAddItemToFolder = () => {
if (
!selectedItem ||
selectedItem.itemType !== NavigationMenuItemType.FOLDER
) {
if (!selectedItem || selectedItem.type !== NavigationMenuItemType.FOLDER) {
return;
}
const folderItemCount = workspaceNavigationMenuItems.filter(
@@ -106,7 +102,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showColorOption={isDefined(selectedItem)}
selectedItem={selectedItem as ProcessedNavigationMenuItem | undefined}
selectedItem={selectedItem}
/>
);
case NavigationMenuItemType.VIEW:
@@ -125,7 +121,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
case NavigationMenuItemType.LINK:
if (
isDefined(selectedItem) &&
selectedItem.itemType === NavigationMenuItemType.LINK
selectedItem.type === NavigationMenuItemType.LINK
) {
return (
<SidePanelEditLinkItemView
@@ -11,7 +11,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
@@ -1,6 +1,5 @@
import { Avatar, IconFolder, useIcons } from 'twenty-ui/display';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
@@ -11,7 +10,10 @@ import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/Si
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import {
CoreObjectNameSingular,
NavigationMenuItemType,
} from 'twenty-shared/types';
type SearchRecord = {
recordId: string;
@@ -1,5 +1,5 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
@@ -4,7 +4,7 @@ import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
@@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsAllFolders';
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
@@ -45,7 +45,7 @@ export const useFolderPickerSelectionData = () => {
selectedNavigationMenuItemInEditModeState,
);
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectedItemType = selectedItem?.itemType ?? null;
const selectedItemType = selectedItem?.type ?? null;
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();