Skip to content

Commit a892462

Browse files
fix(#15950): mobile favorites folder navigation with proper back button (#16118)
## What’s done - Clicking a favorite folder on mobile now opens a clean drill-down layer with only its contents - Added a back button (icon + folder name) — visually 100% identical to `NavigationDrawerBackButton` - Sidebar no longer collapses when opening a folder - Tree line is removed on mobile (consistent with current Twenty design) ## Intentional deviations from the mockup 1. **Back button does not replace `MultiWorkspaceDropdownButton`** Kept `NavigationDrawerHeader` untouched — it’s responsible for the hamburger menu and workspace name. Added the back button as a separate block below. → This feels cleaner architecturally and avoids breaking other places that use `NavigationDrawerHeader`. 2. **Vertical spacing between items is slightly tighter than in the mockup** Used `NavigationDrawerSubItem` — the standard component for nested items in Twenty. Current production spacing matches this exactly, so I preferred consistency over pixel-perfect mockup matching. Happy to adjust either point if the team prefers to follow the mockup 1:1. Closes #15950 --------- Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
1 parent e18262c commit a892462

File tree

6 files changed

+178
-10
lines changed

6 files changed

+178
-10
lines changed

packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/compone
2121
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
2222
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
2323
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
24+
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
2425
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
2526
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
2627
import { Droppable } from '@hello-pangea/dnd';
2728
import { useContext, useState } from 'react';
2829
import { createPortal } from 'react-dom';
2930
import { useLocation } from 'react-router-dom';
30-
import { useRecoilState, useRecoilValue } from 'recoil';
31+
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
3132
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
3233
import { LightIconButton } from 'twenty-ui/input';
3334
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
35+
import { useIsMobile } from 'twenty-ui/utilities';
3436

3537
type CurrentWorkspaceMemberFavoritesProps = {
3638
folder: {
@@ -56,19 +58,30 @@ export const CurrentWorkspaceMemberFavorites = ({
5658
);
5759
const { openModal } = useModal();
5860

61+
const isMobile = useIsMobile();
62+
5963
const [openFavoriteFolderIds, setOpenFavoriteFolderIds] = useRecoilState(
6064
openFavoriteFolderIdsState,
6165
);
66+
67+
const setCurrentFolderId = useSetRecoilState(currentFavoriteFolderIdState);
68+
6269
const isOpen = openFavoriteFolderIds.includes(folder.folderId);
6370

6471
const handleToggle = () => {
65-
setOpenFavoriteFolderIds((currentOpenFolders) => {
66-
if (isOpen) {
67-
return currentOpenFolders.filter((id) => id !== folder.folderId);
68-
} else {
69-
return [...currentOpenFolders, folder.folderId];
70-
}
71-
});
72+
if (isMobile) {
73+
setCurrentFolderId((prev) =>
74+
prev === folder.folderId ? null : folder.folderId,
75+
);
76+
} else {
77+
setOpenFavoriteFolderIds((currentOpenFolders) => {
78+
if (isOpen) {
79+
return currentOpenFolders.filter((id) => id !== folder.folderId);
80+
} else {
81+
return [...currentOpenFolders, folder.folderId];
82+
}
83+
});
84+
}
7285
};
7386

7487
const { renameFavoriteFolder } = useRenameFavoriteFolder();
@@ -173,6 +186,7 @@ export const CurrentWorkspaceMemberFavorites = ({
173186
className="navigation-drawer-item"
174187
isRightOptionsDropdownOpen={isDropdownOpenComponent}
175188
triggerEvent="CLICK"
189+
preventCollapseOnMobile={isMobile}
176190
/>
177191
</FavoritesDroppable>
178192
)}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
2+
import { useTheme } from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
import { useSetRecoilState } from 'recoil';
5+
import { IconX } from 'twenty-ui/display';
6+
7+
const StyledBackButton = styled.button`
8+
display: flex;
9+
align-items: center;
10+
gap: ${({ theme }) => theme.spacing(2)};
11+
padding-left: ${({ theme }) => theme.spacing(1)};
12+
padding-right: ${({ theme }) => theme.spacing(0.5)};
13+
padding-top: ${({ theme }) => theme.spacing(1)};
14+
padding-bottom: ${({ theme }) => theme.spacing(1)};
15+
width: 100%;
16+
background: transparent;
17+
border: none;
18+
color: ${({ theme }) => theme.font.color.secondary};
19+
font-weight: ${({ theme }) => theme.font.weight.medium};
20+
font-family: ${({ theme }) => theme.font.family};
21+
cursor: pointer;
22+
text-align: left;
23+
24+
&:hover {
25+
background: ${({ theme }) => theme.background.transparent.light};
26+
border-radius: ${({ theme }) => theme.border.radius.sm};
27+
}
28+
`;
29+
30+
type FavoritesBackButtonProps = {
31+
folderName: string;
32+
};
33+
34+
export const FavoritesBackButton = ({
35+
folderName,
36+
}: FavoritesBackButtonProps) => {
37+
const theme = useTheme();
38+
const setCurrentFolderId = useSetRecoilState(currentFavoriteFolderIdState);
39+
40+
const handleClick = (e: React.MouseEvent) => {
41+
e.preventDefault();
42+
e.stopPropagation();
43+
setCurrentFolderId(null);
44+
};
45+
46+
return (
47+
<StyledBackButton onClick={handleClick}>
48+
<IconX
49+
size={theme.icon.size.md}
50+
stroke={theme.icon.stroke.lg}
51+
color={theme.font.color.tertiary}
52+
/>
53+
<span>{folderName}</span>
54+
</StyledBackButton>
55+
);
56+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
2+
import { FavoritesBackButton } from '@/favorites/components/FavoritesBackButton';
3+
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
4+
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
5+
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
6+
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
7+
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
8+
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
9+
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
10+
import { Droppable } from '@hello-pangea/dnd';
11+
import { useRecoilValue } from 'recoil';
12+
import { IconHeartOff } from 'twenty-ui/display';
13+
import { LightIconButton } from 'twenty-ui/input';
14+
15+
type FavoritesFolderContentProps = {
16+
folderId: string;
17+
folderName: string;
18+
favorites: ProcessedFavorite[];
19+
};
20+
21+
export const FavoritesFolderContent = ({
22+
folderName,
23+
folderId,
24+
favorites,
25+
}: FavoritesFolderContentProps) => {
26+
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
27+
const { deleteFavorite } = useDeleteFavorite();
28+
29+
return (
30+
<>
31+
<FavoritesBackButton folderName={folderName} />
32+
<FavoritesDragProvider>
33+
<Droppable droppableId={`folder-${folderId}`}>
34+
{(provided) => (
35+
<div
36+
ref={provided.innerRef}
37+
// eslint-disable-next-line react/jsx-props-no-spreading
38+
{...provided.droppableProps}
39+
>
40+
{favorites.map((favorite, index) => (
41+
<DraggableItem
42+
key={favorite.id}
43+
draggableId={favorite.id}
44+
index={index}
45+
isInsideScrollableContainer
46+
itemComponent={
47+
<NavigationDrawerItem
48+
secondaryLabel={getFavoriteSecondaryLabel({
49+
objectMetadataItems,
50+
favoriteObjectNameSingular: favorite.objectNameSingular,
51+
})}
52+
label={favorite.labelIdentifier}
53+
Icon={() => <FavoriteIcon favorite={favorite} />}
54+
rightOptions={
55+
<LightIconButton
56+
Icon={IconHeartOff}
57+
onClick={() => deleteFavorite(favorite.id)}
58+
accent="tertiary"
59+
/>
60+
}
61+
triggerEvent="CLICK"
62+
to={favorite.link}
63+
/>
64+
}
65+
/>
66+
))}
67+
{provided.placeholder}
68+
</div>
69+
)}
70+
</Droppable>
71+
</FavoritesDragProvider>
72+
</>
73+
);
74+
};

packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { useRecoilValue } from 'recoil';
22

33
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
4+
import { FavoritesFolderContent } from '@/favorites/components/FavoritesFolderContent';
5+
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
46
import { MainNavigationDrawerFixedItems } from '@/navigation/components/MainNavigationDrawerFixedItems';
57
import { MainNavigationDrawerScrollableItems } from '@/navigation/components/MainNavigationDrawerScrollableItems';
68
import { SupportDropdown } from '@/support/components/SupportDropdown';
79
import { NavigationDrawer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
810
import { NavigationDrawerFixedContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent';
911
import { NavigationDrawerScrollableContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerScrollableContent';
12+
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
1013

1114
export const MainNavigationDrawer = ({ className }: { className?: string }) => {
1215
const currentWorkspace = useRecoilValue(currentWorkspaceState);
16+
const currentFavoriteFolderId = useRecoilValue(currentFavoriteFolderIdState);
17+
const { favoritesByFolder } = useFavoritesByFolder();
18+
const openedFolder = favoritesByFolder.find(
19+
(f) => f.folderId === currentFavoriteFolderId,
20+
);
1321

1422
return (
1523
<NavigationDrawer
@@ -21,7 +29,15 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
2129
</NavigationDrawerFixedContent>
2230

2331
<NavigationDrawerScrollableContent>
24-
<MainNavigationDrawerScrollableItems />
32+
{currentFavoriteFolderId && openedFolder ? (
33+
<FavoritesFolderContent
34+
folderName={openedFolder.folderName}
35+
folderId={openedFolder.folderId}
36+
favorites={openedFolder.favorites}
37+
/>
38+
) : (
39+
<MainNavigationDrawerScrollableItems />
40+
)}
2541
</NavigationDrawerScrollableContent>
2642

2743
<NavigationDrawerFixedContent>

packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type NavigationDrawerItemProps = {
5151
isRightOptionsDropdownOpen?: boolean;
5252
triggerEvent?: TriggerEventType;
5353
mouseUpNavigation?: boolean;
54+
preventCollapseOnMobile?: boolean;
5455
};
5556

5657
type StyledItemProps = Pick<
@@ -267,6 +268,7 @@ export const NavigationDrawerItem = ({
267268
isRightOptionsDropdownOpen,
268269
triggerEvent,
269270
mouseUpNavigation = false,
271+
preventCollapseOnMobile = false,
270272
}: NavigationDrawerItemProps) => {
271273
const theme = useTheme();
272274
const isMobile = useIsMobile();
@@ -282,7 +284,7 @@ export const NavigationDrawerItem = ({
282284
);
283285

284286
const handleMobileNavigation = () => {
285-
if (isMobile) {
287+
if (isMobile && !preventCollapseOnMobile) {
286288
setIsNavigationDrawerExpanded(false);
287289
}
288290
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { atom } from 'recoil';
2+
3+
export const currentFavoriteFolderIdState = atom<string | null>({
4+
key: 'currentFavoriteFolderIdState',
5+
default: null,
6+
});

0 commit comments

Comments
 (0)