Refactor MainNavigationDrawer to enhance tab navigation and integrate AI chat functionality

- Removed MainNavigationDrawerFixedItems component and replaced it with MainNavigationDrawerTabsRow for improved tab management.
- Introduced AIChatThreadsList within the MainNavigationDrawer for chat tab functionality.
- Added navigationDrawerActiveTabState to manage active tab state.
- Updated NavigationDrawerHeader to include a search button and adjusted styles for better layout consistency.
This commit is contained in:
Abdul Rahman
2026-02-23 02:43:22 +05:30
parent bc20a90a61
commit da3d0ae377
6 changed files with 268 additions and 86 deletions
@@ -1,17 +1,19 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { AIChatThreadsList } from '@/ai/components/AIChatThreadsList';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
import { NavigationMenuItemFolderContentDispatcherEffect } from '@/navigation-menu-item/components/NavigationMenuItemFolderContentDispatcher';
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
import { MainNavigationDrawerFixedItems } from '@/navigation/components/MainNavigationDrawerFixedItems';
import { MainNavigationDrawerScrollableItems } from '@/navigation/components/MainNavigationDrawerScrollableItems';
import { MainNavigationDrawerTabsRow } from '@/navigation/components/MainNavigationDrawerTabsRow';
import { NavigationDrawer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { NavigationDrawerFixedContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent';
import { NavigationDrawerScrollableContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerScrollableContent';
import { currentFavoriteFolderIdStateV2 } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdStateV2';
import { currentNavigationMenuItemFolderIdStateV2 } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdStateV2';
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -21,8 +23,16 @@ const StyledScrollableContent = styled.div`
min-height: 0;
`;
const StyledAIChatThreadsListWrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
`;
export const MainNavigationDrawer = ({ className }: { className?: string }) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const activeTab = useRecoilValueV2(navigationDrawerActiveTabState);
const currentFavoriteFolderId = useRecoilValueV2(
currentFavoriteFolderIdStateV2,
);
@@ -55,11 +65,15 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
title={currentWorkspace?.displayName ?? ''}
>
<NavigationDrawerFixedContent>
<MainNavigationDrawerFixedItems />
<MainNavigationDrawerTabsRow />
</NavigationDrawerFixedContent>
<NavigationDrawerScrollableContent>
{isNavigationMenuItemEditingEnabled ? (
{activeTab === 'chat' ? (
<StyledAIChatThreadsListWrapper>
<AIChatThreadsList />
</StyledAIChatThreadsListWrapper>
) : isNavigationMenuItemEditingEnabled ? (
<StyledScrollableContent>
{openedFolder ? (
<NavigationMenuItemFolderContentDispatcherEffect
@@ -1,72 +0,0 @@
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedStateV2 } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedStateV2';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { useLocation, useNavigate } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconSearch, IconSettings, IconSparkles } from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const MainNavigationDrawerFixedItems = () => {
const isMobile = useIsMobile();
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilStateV2(
navigationMemorizedUrlState,
);
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilStateV2(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilStateV2(
navigationDrawerExpandedMemorizedStateV2,
);
const navigate = useNavigate();
const { t } = useLingui();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
return (
!isMobile && (
<>
<NavigationDrawerItem
label={t`Search`}
Icon={IconSearch}
onClick={openRecordsSearchPage}
keyboard={['/']}
mouseUpNavigation={true}
/>
{isAiEnabled && (
<NavigationDrawerItem
label={t`Ask AI`}
Icon={IconSparkles}
onClick={() => openAskAIPage({ resetNavigationStack: true })}
keyboard={['@']}
mouseUpNavigation={true}
/>
)}
<NavigationDrawerItem
label={t`Settings`}
to={getSettingsPath(SettingsPath.ProfilePage)}
onClick={() => {
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
setIsNavigationDrawerExpanded(true);
setNavigationMemorizedUrl(location.pathname + location.search);
navigate(getSettingsPath(SettingsPath.ProfilePage));
}}
Icon={IconSettings}
/>
</>
)
);
};
@@ -0,0 +1,185 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
IconComment,
IconHome,
IconMessageCirclePlus,
} from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
import {
type NavigationDrawerActiveTab,
navigationDrawerActiveTabState,
} from '@/ui/navigation/states/navigationDrawerActiveTabState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledRow = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledTabsPill = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.pill};
padding: ${({ theme }) => theme.spacing(0.75)};
height: ${({ theme }) => theme.spacing(7)};
display: flex;
width: ${({ theme }) => theme.spacing(18)};
gap: ${({ theme }) => theme.spacing(0.5)};
box-sizing: border-box;
`;
const StyledTabWrapper = styled.div<{ isActive: boolean }>`
border-radius: ${({ theme }) => theme.border.radius.pill};
align-items: center;
background: ${({ theme, isActive }) =>
isActive ? theme.background.transparent.light : 'transparent'};
color: ${({ theme, isActive }) =>
isActive ? theme.font.color.primary : theme.font.color.tertiary};
cursor: pointer;
display: flex;
justify-content: center;
height: 100%;
flex: 1;
&:hover {
background: ${({ theme, isActive }) =>
isActive
? theme.background.transparent.light
: theme.background.transparent.lighter};
}
`;
const StyledTabIcon = styled.div`
align-items: center;
display: flex;
justify-content: center;
height: ${({ theme }) => theme.spacing(5)};
width: ${({ theme }) => theme.spacing(5)};
`;
const StyledNewChatButtonWrapper = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.pill};
height: ${({ theme }) => theme.spacing(7)};
padding: ${({ theme }) => theme.spacing(0.75)};
width: ${({ theme }) => theme.spacing(25.75)};
box-sizing: border-box;
`;
const StyledNewChatButton = styled.div`
align-items: center;
justify-content: center;
display: flex;
cursor: pointer;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
height: 100%;
width: 100%;
border-radius: inherit;
color: ${({ theme }) => theme.font.color.secondary};
transition:
background ${({ theme }) => theme.animation.duration.fast}s ease,
color ${({ theme }) => theme.animation.duration.fast}s ease;
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
color: ${({ theme }) => theme.font.color.primary};
}
`;
export const MainNavigationDrawerTabsRow = () => {
const theme = useTheme();
const isMobile = useIsMobile();
const [activeTab, setActiveTab] = useRecoilStateV2(
navigationDrawerActiveTabState,
);
const { createChatThread } = useCreateNewAIChatThread();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
if (isMobile || !isAiEnabled) {
return null;
}
const handleTabClick = (tab: NavigationDrawerActiveTab) => () => {
setActiveTab(tab);
};
const handleTabKeyDown =
(tab: NavigationDrawerActiveTab) => (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setActiveTab(tab);
}
};
const handleNewChatKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
createChatThread();
}
};
const getTabIconColor = (isActive: boolean) =>
isActive ? theme.font.color.primary : theme.font.color.tertiary;
return (
<StyledRow>
<StyledTabsPill role="tablist" aria-label={t`Navigation tabs`}>
<StyledTabWrapper
isActive={activeTab === 'home'}
role="tab"
aria-selected={activeTab === 'home'}
aria-label={t`Home`}
tabIndex={activeTab === 'home' ? 0 : -1}
onClick={handleTabClick('home')}
onKeyDown={handleTabKeyDown('home')}
>
<StyledTabIcon>
<IconHome
size={theme.icon.size.sm}
color={getTabIconColor(activeTab === 'home')}
/>
</StyledTabIcon>
</StyledTabWrapper>
<StyledTabWrapper
isActive={activeTab === 'chat'}
role="tab"
aria-selected={activeTab === 'chat'}
aria-label={t`Chat`}
tabIndex={activeTab === 'chat' ? 0 : -1}
onClick={handleTabClick('chat')}
onKeyDown={handleTabKeyDown('chat')}
>
<StyledTabIcon>
<IconComment
size={theme.icon.size.sm}
color={getTabIconColor(activeTab === 'chat')}
/>
</StyledTabIcon>
</StyledTabWrapper>
</StyledTabsPill>
<StyledNewChatButtonWrapper>
<StyledNewChatButton
role="button"
tabIndex={0}
aria-label={t`New chat`}
onClick={() => createChatThread()}
onKeyDown={handleNewChatKeyDown}
>
<IconMessageCirclePlus size={theme.icon.size.md} />
{t`New chat`}
</StyledNewChatButton>
</StyledNewChatButtonWrapper>
</StyledRow>
);
};
@@ -19,13 +19,18 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { multiWorkspaceDropdownStateV2 } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownStateV2';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { navigationDrawerExpandedMemorizedStateV2 } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedStateV2';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { type ApolloError } from '@apollo/client';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { useLocation, useNavigate } from 'react-router-dom';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
@@ -35,6 +40,7 @@ import {
IconLogout,
IconMessage,
IconPlus,
IconSettings,
IconSwitchHorizontal,
IconUserPlus,
} from 'twenty-ui/display';
@@ -79,6 +85,17 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
multiWorkspaceDropdownStateV2,
);
const location = useLocation();
const navigate = useNavigate();
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilStateV2(isNavigationDrawerExpandedState);
const setNavigationDrawerExpandedMemorized = useSetRecoilStateV2(
navigationDrawerExpandedMemorizedStateV2,
);
const setNavigationMemorizedUrl = useSetRecoilStateV2(
navigationMemorizedUrlState,
);
const handleSupport = () => {
window.FrontChat?.('show');
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
@@ -225,6 +242,18 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
>
<MenuItem LeftIcon={IconUserPlus} text={t`Invite user`} />
</UndecoratedLink>
<UndecoratedLink
to={getSettingsPath(SettingsPath.ProfilePage)}
onClick={() => {
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
setIsNavigationDrawerExpanded(true);
setNavigationMemorizedUrl(location.pathname + location.search);
navigate(getSettingsPath(SettingsPath.ProfilePage));
closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
}}
>
<MenuItem LeftIcon={IconSettings} text={t`Settings`} />
</UndecoratedLink>
{isSupportChatConfigured && (
<MenuItem
LeftIcon={IconMessage}
@@ -1,10 +1,13 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconSearch } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
@@ -16,15 +19,20 @@ const StyledContainer = styled.div`
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
`;
const StyledRightActions = styled.div`
align-items: center;
display: flex;
margin-left: auto;
`;
const StyledNavigationDrawerCollapseButton = styled(
NavigationDrawerCollapseButton,
)<{ show?: boolean }>`
height: ${({ theme }) => theme.spacing(4)};
margin-left: auto;
height: ${({ theme }) => theme.spacing(6)};
opacity: ${({ show }) => (show ? 1 : 0)};
padding-right: ${({ theme }) => theme.spacing(1)};
transition: opacity ${({ theme }) => theme.animation.duration.normal}s;
width: ${({ theme }) => theme.spacing(4)};
width: ${({ theme }) => theme.spacing(6)};
`;
type NavigationDrawerHeaderProps = {
@@ -35,7 +43,7 @@ export const NavigationDrawerHeader = ({
showCollapseButton,
}: NavigationDrawerHeaderProps) => {
const isMobile = useIsMobile();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const isNavigationDrawerExpanded = useRecoilValueV2(
isNavigationDrawerExpandedState,
);
@@ -44,10 +52,19 @@ export const NavigationDrawerHeader = ({
<StyledContainer>
<MultiWorkspaceDropdownButton />
{!isMobile && isNavigationDrawerExpanded && (
<StyledNavigationDrawerCollapseButton
direction="left"
show={showCollapseButton}
/>
<StyledRightActions>
<LightIconButton
Icon={IconSearch}
accent="tertiary"
size="small"
onClick={openRecordsSearchPage}
aria-label={t`Search`}
/>
<StyledNavigationDrawerCollapseButton
direction="left"
show={showCollapseButton}
/>
</StyledRightActions>
)}
</StyledContainer>
);
@@ -0,0 +1,9 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export type NavigationDrawerActiveTab = 'home' | 'chat';
export const navigationDrawerActiveTabState =
createStateV2<NavigationDrawerActiveTab>({
key: 'navigationDrawerActiveTab',
defaultValue: 'home',
});