Navbar with AI chats (#18161)
## Summary Add Home/Chat tabs and a dedicated threads list in the navigation drawer. ## Changes - **Navbar tabs:** Tabs in the drawer to switch between Home and Chat (with “New chat” button). Shown on desktop when expanded and on mobile below the workspace selector. - **Navbar threads list:** New `NavigationDrawerAIChatThreadsList` for the Chat tab with date groups (Today / Yesterday / Older), thread rows as `NavigationDrawerItem` (IconComment, title, timestamp). Shared `useAIChatThreadClick` hook used by navbar and command menu; navbar passes `resetNavigationStack: true`. - **NavigationDrawerItem:** New `alwaysShowRightOptions` prop so the timestamp is always visible (no hover-only). --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -1,51 +1,19 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import styled from '@emotion/styled';
|
||||
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 { MainNavigationDrawerAIChatContent } from '@/navigation/components/MainNavigationDrawerAIChatContent';
|
||||
import { MainNavigationDrawerNavigationContent } from '@/navigation/components/MainNavigationDrawerNavigationContent';
|
||||
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 { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
|
||||
import { NAVIGATION_DRAWER_TABS } from '@/ui/navigation/states/navigationDrawerTabs';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledScrollableContent = styled.div`
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
const navigationDrawerActiveTab = useAtomStateValue(
|
||||
navigationDrawerActiveTabState,
|
||||
);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const currentFavoriteFolderId = useAtomStateValue(
|
||||
currentFavoriteFolderIdState,
|
||||
);
|
||||
const currentNavigationMenuItemFolderId = useAtomStateValue(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const openedFavoriteFolder = favoritesByFolder.find(
|
||||
(f) => f.folderId === currentFavoriteFolderId,
|
||||
);
|
||||
|
||||
const openedNavigationMenuItemFolder = navigationMenuItemsByFolder.find(
|
||||
(f) => f.id === currentNavigationMenuItemFolderId,
|
||||
);
|
||||
|
||||
const openedFolder = isNavigationMenuItemEditingEnabled
|
||||
? openedNavigationMenuItemFolder
|
||||
: openedFavoriteFolder;
|
||||
|
||||
const openedFolderId = openedNavigationMenuItemFolder?.id ?? '';
|
||||
|
||||
return (
|
||||
<NavigationDrawer
|
||||
@@ -53,36 +21,15 @@ export const MainNavigationDrawer = ({ className }: { className?: string }) => {
|
||||
title={currentWorkspace?.displayName ?? ''}
|
||||
>
|
||||
<NavigationDrawerFixedContent>
|
||||
<MainNavigationDrawerFixedItems />
|
||||
<MainNavigationDrawerTabsRow />
|
||||
</NavigationDrawerFixedContent>
|
||||
|
||||
<NavigationDrawerScrollableContent>
|
||||
{isNavigationMenuItemEditingEnabled ? (
|
||||
<StyledScrollableContent>
|
||||
{openedFolder ? (
|
||||
<NavigationMenuItemFolderContentDispatcherEffect
|
||||
folderName={openedFolder.folderName}
|
||||
folderId={openedFolderId}
|
||||
favorites={openedFavoriteFolder?.favorites}
|
||||
navigationMenuItems={
|
||||
openedNavigationMenuItemFolder?.navigationMenuItems
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MainNavigationDrawerScrollableItems />
|
||||
)}
|
||||
</StyledScrollableContent>
|
||||
) : openedFolder ? (
|
||||
<NavigationMenuItemFolderContentDispatcherEffect
|
||||
folderName={openedFolder.folderName}
|
||||
folderId={openedFolderId}
|
||||
favorites={openedFavoriteFolder?.favorites}
|
||||
navigationMenuItems={
|
||||
openedNavigationMenuItemFolder?.navigationMenuItems
|
||||
}
|
||||
/>
|
||||
{navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY ? (
|
||||
<MainNavigationDrawerAIChatContent />
|
||||
) : (
|
||||
<MainNavigationDrawerScrollableItems />
|
||||
<MainNavigationDrawerNavigationContent />
|
||||
)}
|
||||
</NavigationDrawerScrollableContent>
|
||||
</NavigationDrawer>
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { NavigationDrawerAIChatThreadsList } from '@/ai/components/NavigationDrawerAIChatThreadsList';
|
||||
|
||||
const StyledAIChatThreadsListWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
export const MainNavigationDrawerAIChatContent = () => {
|
||||
return (
|
||||
<StyledAIChatThreadsListWrapper>
|
||||
<NavigationDrawerAIChatThreadsList />
|
||||
</StyledAIChatThreadsListWrapper>
|
||||
);
|
||||
};
|
||||
-72
@@ -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 { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
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 = useSetAtomState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
|
||||
useAtomState(isNavigationDrawerExpandedState);
|
||||
const setNavigationDrawerExpandedMemorized = useSetAtomState(
|
||||
navigationDrawerExpandedMemorizedState,
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { NavigationMenuItemFolderContentDispatcherEffect } from '@/navigation-menu-item/components/NavigationMenuItemFolderContentDispatcher';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
|
||||
import { MainNavigationDrawerScrollableItems } from '@/navigation/components/MainNavigationDrawerScrollableItems';
|
||||
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledScrollableContent = styled.div`
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
export const MainNavigationDrawerNavigationContent = () => {
|
||||
const currentFavoriteFolderId = useAtomStateValue(
|
||||
currentFavoriteFolderIdState,
|
||||
);
|
||||
const currentNavigationMenuItemFolderId = useAtomStateValue(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const openedFavoriteFolder = favoritesByFolder.find(
|
||||
(folder) => folder.folderId === currentFavoriteFolderId,
|
||||
);
|
||||
|
||||
const openedNavigationMenuItemFolder = navigationMenuItemsByFolder.find(
|
||||
(folder) => folder.id === currentNavigationMenuItemFolderId,
|
||||
);
|
||||
|
||||
const openedFolder = isNavigationMenuItemEditingEnabled
|
||||
? openedNavigationMenuItemFolder
|
||||
: openedFavoriteFolder;
|
||||
|
||||
const openedFolderId = openedNavigationMenuItemFolder?.id ?? '';
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
return (
|
||||
<StyledScrollableContent>
|
||||
{openedFolder ? (
|
||||
<NavigationMenuItemFolderContentDispatcherEffect
|
||||
folderName={openedFolder.folderName}
|
||||
folderId={openedFolderId}
|
||||
favorites={openedFavoriteFolder?.favorites}
|
||||
navigationMenuItems={
|
||||
openedNavigationMenuItemFolder?.navigationMenuItems
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MainNavigationDrawerScrollableItems />
|
||||
)}
|
||||
</StyledScrollableContent>
|
||||
);
|
||||
}
|
||||
|
||||
return openedFolder ? (
|
||||
<NavigationMenuItemFolderContentDispatcherEffect
|
||||
folderName={openedFolder.folderName}
|
||||
folderId={openedFolderId}
|
||||
favorites={openedFavoriteFolder?.favorites}
|
||||
navigationMenuItems={openedNavigationMenuItemFolder?.navigationMenuItems}
|
||||
/>
|
||||
) : (
|
||||
<MainNavigationDrawerScrollableItems />
|
||||
);
|
||||
};
|
||||
+3
@@ -3,6 +3,8 @@ import { RemoteNavigationDrawerSection } from '@/object-metadata/components/Remo
|
||||
import styled from '@emotion/styled';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection';
|
||||
|
||||
const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher'
|
||||
@@ -36,6 +38,7 @@ export const MainNavigationDrawerScrollableItems = () => {
|
||||
<WorkspaceNavigationMenuItemsDispatcher />
|
||||
</Suspense>
|
||||
<RemoteNavigationDrawerSection />
|
||||
<NavigationDrawerOtherSection />
|
||||
</StyledScrollableItemsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
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 { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
|
||||
import {
|
||||
type NavigationDrawerActiveTab,
|
||||
NAVIGATION_DRAWER_TABS,
|
||||
} from '@/ui/navigation/states/navigationDrawerTabs';
|
||||
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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledRow = styled.div<{ isExpanded: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: ${({ isExpanded }) =>
|
||||
isExpanded ? 'space-between' : 'center'};
|
||||
gap: ${({ theme, isExpanded }) => (isExpanded ? theme.spacing(2) : 0)};
|
||||
width: 100%;
|
||||
transition: gap ${({ theme }) => theme.animation.duration.normal}s ease;
|
||||
`;
|
||||
|
||||
const StyledTabsPill = styled.div`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
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<{ isExpanded: boolean }>`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.pill};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
height: ${({ theme, isExpanded }) =>
|
||||
isExpanded ? theme.spacing(7) : theme.spacing(6)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme, isExpanded }) =>
|
||||
isExpanded ? theme.spacing(0.75) : theme.spacing(0.5)};
|
||||
width: ${({ theme, isExpanded }) =>
|
||||
isExpanded ? theme.spacing(25.75) : theme.spacing(6)};
|
||||
transition:
|
||||
height ${({ theme }) => theme.animation.duration.normal}s ease,
|
||||
padding ${({ theme }) => theme.animation.duration.normal}s ease;
|
||||
`;
|
||||
|
||||
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 isNavigationDrawerExpanded = useAtomStateValue(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
const [navigationDrawerActiveTab, setNavigationDrawerActiveTab] =
|
||||
useAtomState(navigationDrawerActiveTabState);
|
||||
const { createChatThread } = useCreateNewAIChatThread();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const setIsNavigationDrawerExpanded = useSetAtomState(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
|
||||
if (!isAiEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isExpanded = isNavigationDrawerExpanded || isMobile;
|
||||
|
||||
const handleTabClick = (tab: NavigationDrawerActiveTab) => () => {
|
||||
setNavigationDrawerActiveTab(tab);
|
||||
};
|
||||
|
||||
const handleTabKeyDown =
|
||||
(tab: NavigationDrawerActiveTab) => (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
setNavigationDrawerActiveTab(tab);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewChatClick = () => {
|
||||
if (isMobile) {
|
||||
setIsNavigationDrawerExpanded(false);
|
||||
}
|
||||
createChatThread();
|
||||
};
|
||||
|
||||
const handleNewChatKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleNewChatClick();
|
||||
}
|
||||
};
|
||||
|
||||
const getTabIconColor = (isActive: boolean) =>
|
||||
isActive ? theme.font.color.primary : theme.font.color.tertiary;
|
||||
|
||||
return (
|
||||
<StyledRow isExpanded={isExpanded}>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledTabsPill role="tablist" aria-label={t`Navigation tabs`}>
|
||||
<StyledTabWrapper
|
||||
isActive={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
||||
}
|
||||
role="tab"
|
||||
aria-selected={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
||||
}
|
||||
aria-label={t`Home`}
|
||||
tabIndex={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
||||
? 0
|
||||
: -1
|
||||
}
|
||||
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
|
||||
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
|
||||
>
|
||||
<StyledTabIcon>
|
||||
<IconHome
|
||||
size={theme.icon.size.sm}
|
||||
color={getTabIconColor(
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU,
|
||||
)}
|
||||
/>
|
||||
</StyledTabIcon>
|
||||
</StyledTabWrapper>
|
||||
<StyledTabWrapper
|
||||
isActive={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
||||
}
|
||||
role="tab"
|
||||
aria-selected={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
||||
}
|
||||
aria-label={t`Chat`}
|
||||
tabIndex={
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
||||
? 0
|
||||
: -1
|
||||
}
|
||||
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
|
||||
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
|
||||
>
|
||||
<StyledTabIcon>
|
||||
<IconComment
|
||||
size={theme.icon.size.sm}
|
||||
color={getTabIconColor(
|
||||
navigationDrawerActiveTab ===
|
||||
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY,
|
||||
)}
|
||||
/>
|
||||
</StyledTabIcon>
|
||||
</StyledTabWrapper>
|
||||
</StyledTabsPill>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledNewChatButtonWrapper isExpanded={isExpanded}>
|
||||
<StyledNewChatButton
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t`New chat`}
|
||||
onClick={handleNewChatClick}
|
||||
onKeyDown={handleNewChatKeyDown}
|
||||
>
|
||||
<IconMessageCirclePlus size={theme.icon.size.md} />
|
||||
{isExpanded && t`New chat`}
|
||||
</StyledNewChatButton>
|
||||
</StyledNewChatButtonWrapper>
|
||||
</StyledRow>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,29 @@
|
||||
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
type IconComponent,
|
||||
IconList,
|
||||
IconMessageCirclePlus,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
import { NavigationBar } from 'twenty-ui/navigation';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
|
||||
type NavigationBarItemName = 'main' | 'search' | 'newAIChat';
|
||||
|
||||
export const MobileNavigationBar = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -34,7 +36,8 @@ export const MobileNavigationBar = () => {
|
||||
useAtomState(isNavigationDrawerExpandedState);
|
||||
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
|
||||
useAtomState(currentMobileNavigationDrawerState);
|
||||
const { openSettingsMenu } = useOpenSettingsMenu();
|
||||
const { createChatThread } = useCreateNewAIChatThread();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const { alphaSortedActiveNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
@@ -47,9 +50,7 @@ export const MobileNavigationBar = () => {
|
||||
? currentMobileNavigationDrawer
|
||||
: isCommandMenuOpened
|
||||
? 'search'
|
||||
: isSettingsPage
|
||||
? 'settings'
|
||||
: 'main';
|
||||
: 'main';
|
||||
|
||||
const items: {
|
||||
name: NavigationBarItemName;
|
||||
@@ -91,14 +92,19 @@ export const MobileNavigationBar = () => {
|
||||
openRecordsSearchPage();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
Icon: IconSettings,
|
||||
onClick: () => {
|
||||
closeCommandMenu();
|
||||
openSettingsMenu();
|
||||
},
|
||||
},
|
||||
...(isAiEnabled
|
||||
? [
|
||||
{
|
||||
name: 'newAIChat' as const,
|
||||
Icon: IconMessageCirclePlus,
|
||||
onClick: () => {
|
||||
setIsNavigationDrawerExpanded(false);
|
||||
closeCommandMenu();
|
||||
createChatThread();
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return <NavigationBar activeItemName={activeItemName} items={items} />;
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
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 { IconHelpCircle, IconSettings } from 'twenty-ui/display';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
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 { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
|
||||
export const NavigationDrawerOtherSection = () => {
|
||||
const { t } = useLingui();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
|
||||
useAtomState(isNavigationDrawerExpandedState);
|
||||
const setNavigationDrawerExpandedMemorized = useSetAtomState(
|
||||
navigationDrawerExpandedMemorizedState,
|
||||
);
|
||||
const setNavigationMemorizedUrl = useSetAtomState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const { toggleNavigationSection } = useNavigationSection('Other');
|
||||
const isNavigationSectionOpen = useAtomFamilyStateValue(
|
||||
isNavigationSectionOpenFamilyState,
|
||||
'Other',
|
||||
);
|
||||
|
||||
const handleSettingsClick = () => {
|
||||
setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded);
|
||||
setIsNavigationDrawerExpanded(true);
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
navigate(getSettingsPath(SettingsPath.ProfilePage));
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={t`Other`}
|
||||
onClick={toggleNavigationSection}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
Icon={IconSettings}
|
||||
onClick={handleSettingsClick}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Documentation`}
|
||||
to={getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
})}
|
||||
Icon={IconHelpCircle}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user