From da3d0ae377c42217848fe2a05b72e7dfb51cc16d Mon Sep 17 00:00:00 2001 From: Abdul Rahman Date: Mon, 23 Feb 2026 02:43:22 +0530 Subject: [PATCH] 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. --- .../components/MainNavigationDrawer.tsx | 20 +- .../MainNavigationDrawerFixedItems.tsx | 72 ------- .../MainNavigationDrawerTabsRow.tsx | 185 ++++++++++++++++++ ...ultiWorkspaceDropdownDefaultComponents.tsx | 29 +++ .../components/NavigationDrawerHeader.tsx | 39 ++-- .../states/navigationDrawerActiveTabState.ts | 9 + 6 files changed, 268 insertions(+), 86 deletions(-) delete mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx create mode 100644 packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerActiveTabState.ts diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx index 8ebdd7b4424..9576b018dfb 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx @@ -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 ?? ''} > - + - {isNavigationMenuItemEditingEnabled ? ( + {activeTab === 'chat' ? ( + + + + ) : isNavigationMenuItemEditingEnabled ? ( {openedFolder ? ( { - 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 && ( - <> - - {isAiEnabled && ( - openAskAIPage({ resetNavigationStack: true })} - keyboard={['@']} - mouseUpNavigation={true} - /> - )} - { - setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded); - setIsNavigationDrawerExpanded(true); - setNavigationMemorizedUrl(location.pathname + location.search); - navigate(getSettingsPath(SettingsPath.ProfilePage)); - }} - Icon={IconSettings} - /> - - ) - ); -}; diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx new file mode 100644 index 00000000000..eb8f51efbad --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx @@ -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 ( + + + + + + + + + + + + + + + createChatThread()} + onKeyDown={handleNewChatKeyDown} + > + + {t`New chat`} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx index 724b47fca93..09b4d71c869 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx @@ -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 = () => { > + { + setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded); + setIsNavigationDrawerExpanded(true); + setNavigationMemorizedUrl(location.pathname + location.search); + navigate(getSettingsPath(SettingsPath.ProfilePage)); + closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); + }} + > + + {isSupportChatConfigured && ( ` - 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 = ({ {!isMobile && isNavigationDrawerExpanded && ( - + + + + )} ); diff --git a/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerActiveTabState.ts b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerActiveTabState.ts new file mode 100644 index 00000000000..8bbda03791a --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerActiveTabState.ts @@ -0,0 +1,9 @@ +import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2'; + +export type NavigationDrawerActiveTab = 'home' | 'chat'; + +export const navigationDrawerActiveTabState = + createStateV2({ + key: 'navigationDrawerActiveTab', + defaultValue: 'home', + });