Fix view favorite making the app crash (#15517)

On new workspaces, as they come without the "view" object which not part
of engine metadata, favorites pointing to views make the app crashes.

This fixes it
This commit is contained in:
Charles Bochet
2025-11-01 20:06:54 +01:00
committed by Charles Bochet
parent 4c93c90ea1
commit 087a4e384d
6 changed files with 75 additions and 112 deletions
@@ -1,6 +1,5 @@
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
import { FavoriteNavigationDrawerSubItem } from '@/favorites/components/FavoriteNavigationDrawerSubItem';
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
import { FAVORITE_FOLDER_DELETE_MODAL_ID } from '@/favorites/constants/FavoriteFolderDeleteModalId';
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
@@ -8,8 +7,10 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
@@ -19,13 +20,14 @@ import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpe
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
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';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { Droppable } from '@hello-pangea/dnd';
import { useContext, useState } from 'react';
import { createPortal } from 'react-dom';
import { useLocation } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
@@ -43,6 +45,7 @@ export const CurrentWorkspaceMemberFavorites = ({
folder,
isGroup,
}: CurrentWorkspaceMemberFavoritesProps) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentPath = useLocation().pathname;
const currentViewPath = useLocation().pathname + useLocation().search;
const { isDragging } = useContext(FavoritesDragContext);
@@ -196,8 +199,12 @@ export const CurrentWorkspaceMemberFavorites = ({
index={index}
isInsideScrollableContainer
itemComponent={
<FavoriteNavigationDrawerSubItem
favorite={favorite}
<NavigationDrawerSubItem
secondaryLabel={getFavoriteSecondaryLabel({
objectMetadataItems,
favoriteObjectNameSingular:
favorite.objectNameSingular,
})}
label={favorite.labelIdentifier}
Icon={() => <FavoriteIcon favorite={favorite} />}
to={isDragging ? undefined : favorite.link}
@@ -1,14 +1,17 @@
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
import { FavoriteNavigationDrawerItem } from '@/favorites/components/FavoriteNavigationDrawerItem';
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
@@ -21,6 +24,7 @@ const StyledOrphanFavoritesContainer = styled.div`
`;
export const CurrentWorkspaceMemberOrphanFavorites = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const currentPath = useLocation().pathname;
@@ -42,8 +46,11 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanFavoritesContainer>
<FavoriteNavigationDrawerItem
favorite={favorite}
<NavigationDrawerItem
secondaryLabel={getFavoriteSecondaryLabel({
objectMetadataItems,
favoriteObjectNameSingular: favorite.objectNameSingular,
})}
label={favorite.labelIdentifier}
Icon={() => <FavoriteIcon favorite={favorite} />}
active={isLocationMatchingFavorite(
@@ -1,50 +0,0 @@
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
type FavoriteNavigationDrawerItemProps = {
favorite: ProcessedFavorite;
label: string;
Icon?: React.ComponentProps<typeof NavigationDrawerItem>['Icon'];
to?: string;
active?: boolean;
rightOptions?: React.ComponentProps<
typeof NavigationDrawerItem
>['rightOptions'];
isDragging?: boolean;
triggerEvent?: React.ComponentProps<
typeof NavigationDrawerItem
>['triggerEvent'];
};
export const FavoriteNavigationDrawerItem = ({
favorite,
label,
Icon,
to,
active,
rightOptions,
isDragging,
triggerEvent,
}: FavoriteNavigationDrawerItemProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: favorite.objectNameSingular,
});
if (!favorite.objectNameSingular) {
return null;
}
return (
<NavigationDrawerItem
label={label}
secondaryLabel={objectMetadataItem.labelSingular}
Icon={Icon}
to={to}
active={active}
rightOptions={rightOptions}
isDragging={isDragging}
triggerEvent={triggerEvent}
/>
);
};
@@ -1,55 +0,0 @@
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
type FavoriteNavigationDrawerSubItemProps = {
favorite: ProcessedFavorite;
label: string;
Icon?: React.ComponentProps<typeof NavigationDrawerSubItem>['Icon'];
to?: string;
active?: boolean;
subItemState?: React.ComponentProps<
typeof NavigationDrawerSubItem
>['subItemState'];
rightOptions?: React.ComponentProps<
typeof NavigationDrawerSubItem
>['rightOptions'];
isDragging?: boolean;
triggerEvent?: React.ComponentProps<
typeof NavigationDrawerSubItem
>['triggerEvent'];
};
export const FavoriteNavigationDrawerSubItem = ({
favorite,
label,
Icon,
to,
active,
subItemState,
rightOptions,
isDragging,
triggerEvent,
}: FavoriteNavigationDrawerSubItemProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: favorite.objectNameSingular || '',
});
if (!favorite.objectNameSingular) {
return null;
}
return (
<NavigationDrawerSubItem
label={label}
secondaryLabel={objectMetadataItem.labelSingular}
Icon={Icon}
to={to}
active={active}
subItemState={subItemState}
rightOptions={rightOptions}
isDragging={isDragging}
triggerEvent={triggerEvent}
/>
);
};
@@ -0,0 +1,31 @@
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getFavoriteSecondaryLabel } from '../getFavoriteSecondaryLabel';
describe('getFavoriteSecondaryLabel', () => {
it('should return "View" for view object', () => {
const result = getFavoriteSecondaryLabel({
objectMetadataItems: generatedMockObjectMetadataItems,
favoriteObjectNameSingular: 'view',
});
expect(result).toBe('View');
});
it('should return labelSingular for matching object metadata item', () => {
const result = getFavoriteSecondaryLabel({
objectMetadataItems: generatedMockObjectMetadataItems,
favoriteObjectNameSingular: 'person',
});
expect(result).toBe('Person');
});
it('should return undefined when object metadata item is not found', () => {
const result = getFavoriteSecondaryLabel({
objectMetadataItems: generatedMockObjectMetadataItems,
favoriteObjectNameSingular: 'nonexistent',
});
expect(result).toBeUndefined();
});
});
@@ -0,0 +1,23 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type GetFavoriteSecondaryLabelProps = {
objectMetadataItems: Pick<
ObjectMetadataItem,
'nameSingular' | 'labelSingular'
>[];
favoriteObjectNameSingular: string;
};
export const getFavoriteSecondaryLabel = ({
objectMetadataItems,
favoriteObjectNameSingular,
}: GetFavoriteSecondaryLabelProps) => {
if (favoriteObjectNameSingular === 'view') {
return 'View';
}
return objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === favoriteObjectNameSingular,
)?.labelSingular;
};