From e806d36099d48b3fb8a22ef3669f083a71925049 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Sat, 28 Feb 2026 04:07:14 +0530 Subject: [PATCH] Navbar with AI chats (#18161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Co-authored-by: Charles Bochet --- .../ai/components/AIChatThreadGroup.tsx | 44 +--- ...avigationDrawerAIChatThreadDateSection.tsx | 75 ++++++ .../NavigationDrawerAIChatThreadsList.tsx | 55 ++++ .../modules/ai/hooks/useAIChatThreadClick.ts | 53 ++++ .../__tests__/groupThreadsByDate.test.ts | 22 +- .../src/modules/ai/utils/dateGroupKey.ts | 1 + .../src/modules/ai/utils/dateGroupKeys.ts | 7 + .../src/modules/ai/utils/getDateGroupTitle.ts | 14 + .../modules/ai/utils/groupThreadsByDate.ts | 14 +- .../components/CommandMenuOpenContainer.tsx | 2 + .../components/MainNavigationDrawer.tsx | 79 +----- .../MainNavigationDrawerAIChatContent.tsx | 18 ++ .../MainNavigationDrawerFixedItems.tsx | 72 ----- .../MainNavigationDrawerNavigationContent.tsx | 74 ++++++ .../MainNavigationDrawerScrollableItems.tsx | 3 + .../MainNavigationDrawerTabsRow.tsx | 247 ++++++++++++++++++ .../components/MobileNavigationBar.tsx | 42 +-- .../NavigationDrawerOtherSection.tsx | 77 ++++++ ...ultiWorkspaceDropdownDefaultComponents.tsx | 56 ++-- .../components/NavigationDrawer.tsx | 37 ++- .../NavigationDrawerCollapseButton.tsx | 27 +- .../components/NavigationDrawerHeader.tsx | 54 ++-- .../components/NavigationDrawerItem.tsx | 9 +- .../NavigationDrawerClickOutsideId.ts | 1 + .../states/navigationDrawerActiveTabState.ts | 12 + .../navigation/states/navigationDrawerTabs.ts | 7 + .../src/utils/__tests__/date-utils.test.ts | 61 +++++ packages/twenty-front/src/utils/date-utils.ts | 37 +++ ...fix-ai-entity-timestamps-to-timestamptz.ts | 49 ++++ .../entities/agent-message-part.entity.ts | 2 +- .../entities/agent-message.entity.ts | 2 +- .../entities/agent-turn.entity.ts | 2 +- .../entities/agent-turn-evaluation.entity.ts | 2 +- .../entities/agent-chat-thread.entity.ts | 4 +- 34 files changed, 970 insertions(+), 291 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadDateSection.tsx create mode 100644 packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadsList.tsx create mode 100644 packages/twenty-front/src/modules/ai/hooks/useAIChatThreadClick.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/dateGroupKey.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/dateGroupKeys.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/getDateGroupTitle.ts create mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerAIChatContent.tsx delete mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerNavigationContent.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx create mode 100644 packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId.ts create mode 100644 packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerActiveTabState.ts create mode 100644 packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerTabs.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1771600000000-fix-ai-entity-timestamps-to-timestamptz.ts diff --git a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx index 5854aa5494..83a5549f63 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx @@ -1,13 +1,7 @@ -import { agentChatUsageState } from '@/ai/states/agentChatUsageState'; -import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; -import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; -import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { useAIChatThreadClick } from '@/ai/hooks/useAIChatThreadClick'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; -import { isDefined } from 'twenty-shared/utils'; import { IconSparkles } from 'twenty-ui/display'; import { type AgentChatThread } from '~/generated-metadata/graphql'; @@ -24,7 +18,7 @@ const StyledDateGroup = styled.div` const StyledDateHeader = styled.div` color: ${({ theme }) => theme.font.color.light}; font-size: ${({ theme }) => theme.font.size.xs}; - font-weight: 600; + font-weight: ${({ theme }) => theme.font.weight.medium}; margin-bottom: ${({ theme }) => theme.spacing(1)}; `; @@ -79,39 +73,7 @@ export const AIChatThreadGroup = ({ }) => { const { t } = useLingui(); const theme = useTheme(); - const [, setCurrentAIChatThread] = useAtomState(currentAIChatThreadState); - const setCurrentAIChatThreadTitle = useSetAtomState( - currentAIChatThreadTitleState, - ); - const setAgentChatUsage = useSetAtomState(agentChatUsageState); - const { openAskAIPage } = useOpenAskAIPageInCommandMenu(); - - const handleThreadClick = (thread: AgentChatThread) => { - setCurrentAIChatThread(thread.id); - setCurrentAIChatThreadTitle(thread.title ?? null); - - const hasUsageData = - (thread.conversationSize ?? 0) > 0 && - isDefined(thread.contextWindowTokens); - - setAgentChatUsage( - hasUsageData - ? { - lastMessage: null, - conversationSize: thread.conversationSize ?? 0, - contextWindowTokens: thread.contextWindowTokens ?? 0, - inputTokens: thread.totalInputTokens, - outputTokens: thread.totalOutputTokens, - inputCredits: thread.totalInputCredits, - outputCredits: thread.totalOutputCredits, - } - : null, - ); - - openAskAIPage({ - resetNavigationStack: false, - }); - }; + const { handleThreadClick } = useAIChatThreadClick(); if (threads.length === 0) { return null; diff --git a/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadDateSection.tsx b/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadDateSection.tsx new file mode 100644 index 0000000000..e2f22de51d --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadDateSection.tsx @@ -0,0 +1,75 @@ +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { IconComment } from 'twenty-ui/display'; + +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { type AgentChatThread } from '~/generated-metadata/graphql'; +import { beautifyPastDateRelativeToNowShort } from '~/utils/date-utils'; + +const StyledDateSection = styled.section` + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledThreadList = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledDateHeader = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(0, 2)}; +`; + +const StyledThreadTimestamp = styled.span` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + padding-right: ${({ theme }) => theme.spacing(0.5)}; +`; + +export type NavigationDrawerAIChatThreadDateSectionProps = { + title: string; + threads: AgentChatThread[]; + currentThreadId: string | null; + onThreadClick: (thread: AgentChatThread) => void; +}; + +export const NavigationDrawerAIChatThreadDateSection = ({ + title, + threads, + currentThreadId, + onThreadClick, +}: NavigationDrawerAIChatThreadDateSectionProps) => { + const { t } = useLingui(); + + return ( + + {title} + + {threads.map((thread) => { + const isActive = currentThreadId === thread.id; + const timestamp = beautifyPastDateRelativeToNowShort( + thread.updatedAt ?? thread.createdAt, + ); + return ( + onThreadClick(thread)} + alwaysShowRightOptions + rightOptions={ + {timestamp} + } + /> + ); + })} + + + ); +}; diff --git a/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadsList.tsx b/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadsList.tsx new file mode 100644 index 0000000000..7ab9b59c27 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/NavigationDrawerAIChatThreadsList.tsx @@ -0,0 +1,55 @@ +import styled from '@emotion/styled'; + +import { NavigationDrawerAIChatThreadDateSection } from '@/ai/components/NavigationDrawerAIChatThreadDateSection'; +import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader'; +import { useAIChatThreadClick } from '@/ai/hooks/useAIChatThreadClick'; +import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; +import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate'; +import { type DateGroupKey } from '@/ai/utils/dateGroupKey'; +import { DATE_GROUP_KEYS } from '@/ai/utils/dateGroupKeys'; +import { getDateGroupTitle } from '@/ai/utils/getDateGroupTitle'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useGetChatThreadsQuery } from '~/generated-metadata/graphql'; + +const StyledScrollableList = styled.div` + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow-y: auto; + padding: ${({ theme }) => theme.spacing(2, 0)}; + width: ${({ theme }) => `calc(100% - ${theme.spacing(2)})`}; +`; + +export const NavigationDrawerAIChatThreadsList = () => { + const currentAIChatThread = useAtomStateValue(currentAIChatThreadState); + const { handleThreadClick } = useAIChatThreadClick({ + resetNavigationStack: true, + }); + + const { data: { chatThreads = [] } = {}, loading } = useGetChatThreadsQuery(); + const groupedThreads = groupThreadsByDate(chatThreads); + + if (loading === true) { + return ; + } + + return ( + + {DATE_GROUP_KEYS.map((key: DateGroupKey) => { + const threads = groupedThreads[key]; + if (threads.length === 0) return null; + + return ( + + ); + })} + + ); +}; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAIChatThreadClick.ts b/packages/twenty-front/src/modules/ai/hooks/useAIChatThreadClick.ts new file mode 100644 index 0000000000..2bf1814923 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/hooks/useAIChatThreadClick.ts @@ -0,0 +1,53 @@ +import { agentChatUsageState } from '@/ai/states/agentChatUsageState'; +import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState'; +import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState'; +import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { isDefined } from 'twenty-shared/utils'; +import { type AgentChatThread } from '~/generated-metadata/graphql'; + +export type UseAIChatThreadClickOptions = { + resetNavigationStack?: boolean; +}; + +export const useAIChatThreadClick = ( + options: UseAIChatThreadClickOptions = {}, +) => { + const { resetNavigationStack = false } = options; + const [, setCurrentAIChatThread] = useAtomState(currentAIChatThreadState); + const setCurrentAIChatThreadTitle = useSetAtomState( + currentAIChatThreadTitleState, + ); + const setAgentChatUsage = useSetAtomState(agentChatUsageState); + const { openAskAIPage } = useOpenAskAIPageInCommandMenu(); + + const handleThreadClick = (thread: AgentChatThread) => { + setCurrentAIChatThread(thread.id); + setCurrentAIChatThreadTitle(thread.title ?? null); + + const hasUsageData = + (thread.conversationSize ?? 0) > 0 && + isDefined(thread.contextWindowTokens); + + setAgentChatUsage( + hasUsageData + ? { + lastMessage: null, + conversationSize: thread.conversationSize ?? 0, + contextWindowTokens: thread.contextWindowTokens ?? 0, + inputTokens: thread.totalInputTokens, + outputTokens: thread.totalOutputTokens, + inputCredits: thread.totalInputCredits, + outputCredits: thread.totalOutputCredits, + } + : null, + ); + + openAskAIPage({ + resetNavigationStack, + }); + }; + + return { handleThreadClick }; +}; diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts index c845f9bae1..f56546dff3 100644 --- a/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts +++ b/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts @@ -2,9 +2,15 @@ import { type AgentChatThread } from '~/generated-metadata/graphql'; import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate'; describe('groupThreadsByDate', () => { - const baseThread: Omit = { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const twoDaysAgo = new Date(today); + twoDaysAgo.setDate(today.getDate() - 2); + + const baseThread: Omit = { title: 'Test Thread', - updatedAt: new Date().toISOString(), + createdAt: twoDaysAgo.toISOString(), totalInputTokens: 0, totalOutputTokens: 0, contextWindowTokens: null, @@ -13,16 +19,10 @@ describe('groupThreadsByDate', () => { totalOutputCredits: 0, }; - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); - const twoDaysAgo = new Date(today); - twoDaysAgo.setDate(today.getDate() - 2); - const threads: AgentChatThread[] = [ - { ...baseThread, id: '1', createdAt: today.toISOString() }, - { ...baseThread, id: '2', createdAt: yesterday.toISOString() }, - { ...baseThread, id: '3', createdAt: twoDaysAgo.toISOString() }, + { ...baseThread, id: '1', updatedAt: today.toISOString() }, + { ...baseThread, id: '2', updatedAt: yesterday.toISOString() }, + { ...baseThread, id: '3', updatedAt: twoDaysAgo.toISOString() }, ]; it('groups threads into today, yesterday, and older', () => { diff --git a/packages/twenty-front/src/modules/ai/utils/dateGroupKey.ts b/packages/twenty-front/src/modules/ai/utils/dateGroupKey.ts new file mode 100644 index 0000000000..c12765471e --- /dev/null +++ b/packages/twenty-front/src/modules/ai/utils/dateGroupKey.ts @@ -0,0 +1 @@ +export type DateGroupKey = 'today' | 'yesterday' | 'older'; diff --git a/packages/twenty-front/src/modules/ai/utils/dateGroupKeys.ts b/packages/twenty-front/src/modules/ai/utils/dateGroupKeys.ts new file mode 100644 index 0000000000..ede6d99754 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/utils/dateGroupKeys.ts @@ -0,0 +1,7 @@ +import { type DateGroupKey } from '@/ai/utils/dateGroupKey'; + +export const DATE_GROUP_KEYS: readonly DateGroupKey[] = [ + 'today', + 'yesterday', + 'older', +]; diff --git a/packages/twenty-front/src/modules/ai/utils/getDateGroupTitle.ts b/packages/twenty-front/src/modules/ai/utils/getDateGroupTitle.ts new file mode 100644 index 0000000000..58ac9a796a --- /dev/null +++ b/packages/twenty-front/src/modules/ai/utils/getDateGroupTitle.ts @@ -0,0 +1,14 @@ +import { t } from '@lingui/core/macro'; + +import { type DateGroupKey } from '@/ai/utils/dateGroupKey'; + +export const getDateGroupTitle = (key: DateGroupKey): string => { + switch (key) { + case 'today': + return t`Today`; + case 'yesterday': + return t`Yesterday`; + case 'older': + return t`Older`; + } +}; diff --git a/packages/twenty-front/src/modules/ai/utils/groupThreadsByDate.ts b/packages/twenty-front/src/modules/ai/utils/groupThreadsByDate.ts index 2cacf2fa66..e1ffd59ff8 100644 --- a/packages/twenty-front/src/modules/ai/utils/groupThreadsByDate.ts +++ b/packages/twenty-front/src/modules/ai/utils/groupThreadsByDate.ts @@ -1,17 +1,17 @@ import { type AgentChatThread } from '~/generated-metadata/graphql'; -export const groupThreadsByDate = (threads: AgentChatThread[]) => { +import { type DateGroupKey } from '@/ai/utils/dateGroupKey'; + +export const groupThreadsByDate = ( + threads: AgentChatThread[], +): Record => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); - return threads.reduce<{ - today: AgentChatThread[]; - yesterday: AgentChatThread[]; - older: AgentChatThread[]; - }>( + return threads.reduce>( (acc, thread) => { - const threadDate = new Date(thread.createdAt); + const threadDate = new Date(thread.updatedAt); const threadDateString = threadDate.toDateString(); if (threadDateString === today.toDateString()) { diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx index 94400b5ece..50421f05c4 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx @@ -8,6 +8,7 @@ import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/const import { MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/MentionMenuDropdownClickOutsideId'; import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId'; import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; +import { NAVIGATION_DRAWER_CLICK_OUTSIDE_ID } from '@/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId'; import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId'; import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; @@ -76,6 +77,7 @@ export const CommandMenuOpenContainer = ({ callback: handleClickOutside, listenerId: 'COMMAND_MENU_LISTENER_ID', excludedClickOutsideIds: [ + NAVIGATION_DRAWER_CLICK_OUTSIDE_ID, PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID, LINK_CHIP_CLICK_OUTSIDE_ID, RECORD_CHIP_CLICK_OUTSIDE_ID, diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx index 26e242640a..dc5989efb4 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawer.tsx @@ -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 ( { title={currentWorkspace?.displayName ?? ''} > - + - {isNavigationMenuItemEditingEnabled ? ( - - {openedFolder ? ( - - ) : ( - - )} - - ) : openedFolder ? ( - + {navigationDrawerActiveTab === + NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY ? ( + ) : ( - + )} diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerAIChatContent.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerAIChatContent.tsx new file mode 100644 index 0000000000..313a9bea46 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerAIChatContent.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx deleted file mode 100644 index 2502f1d0fb..0000000000 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx +++ /dev/null @@ -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 && ( - <> - - {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/MainNavigationDrawerNavigationContent.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerNavigationContent.tsx new file mode 100644 index 0000000000..f284731abf --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerNavigationContent.tsx @@ -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 ( + + {openedFolder ? ( + + ) : ( + + )} + + ); + } + + return openedFolder ? ( + + ) : ( + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx index 98084c3794..c4b5bb7f92 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx @@ -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 = () => { + ); }; 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 0000000000..16c63477cb --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx @@ -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 ( + + + + + + + + + + + + + + + + + + + {isExpanded && t`New chat`} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx index 110db7087e..8c9ccc6699 100644 --- a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx @@ -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 ; diff --git a/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx b/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx new file mode 100644 index 0000000000..a28626b668 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx @@ -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 ( + + + + + {isNavigationSectionOpen && ( + <> + + + + )} + + ); +}; 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 e287ca5610..1db4f45d4c 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 @@ -2,13 +2,11 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta import { useAuth } from '@/auth/hooks/useAuth'; import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils'; +import { supportChatState } from '@/client-config/states/supportChatState'; import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; -import { supportChatState } from '@/client-config/states/supportChatState'; -import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -19,22 +17,27 @@ 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 { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; +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 { type ApolloError } from '@apollo/client'; import styled from '@emotion/styled'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { isNonEmptyString } from '@sniptt/guards'; import { useLingui } from '@lingui/react/macro'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useLocation } from 'react-router-dom'; import { AppPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { Avatar, IconDotsVertical, - IconHelpCircle, IconLogout, IconMessage, IconPlus, + IconSettings, IconSwitchHorizontal, IconUserPlus, } from 'twenty-ui/display'; @@ -57,7 +60,6 @@ const StyledDescription = styled.div` export const MultiWorkspaceDropdownDefaultComponents = () => { const currentWorkspace = useAtomStateValue(currentWorkspaceState); - const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); const { t } = useLingui(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const availableWorkspaces = useAtomStateValue(availableWorkspacesState); @@ -79,19 +81,21 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { multiWorkspaceDropdownState, ); + const location = useLocation(); + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useAtomState(isNavigationDrawerExpandedState); + const setNavigationDrawerExpandedMemorized = useSetAtomState( + navigationDrawerExpandedMemorizedState, + ); + const setNavigationMemorizedUrl = useSetAtomState( + navigationMemorizedUrlState, + ); + const handleSupport = () => { window.FrontChat?.('show'); closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); }; - const handleDocumentation = () => { - window.open( - getDocumentationUrl({ locale: currentWorkspaceMember?.locale }), - '_blank', - ); - closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); - }; - const handleChange = async (availableWorkspace: AvailableWorkspace) => { redirectToWorkspaceDomain( getWorkspaceUrl(availableWorkspace.workspaceUrls), @@ -149,6 +153,11 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { text={t`Create Workspace`} onClick={createWorkspace} /> + } @@ -223,6 +232,17 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { > + { + setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded); + setIsNavigationDrawerExpanded(true); + setNavigationMemorizedUrl(location.pathname + location.search); + closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); + }} + > + + {isSupportChatConfigured && ( { onClick={handleSupport} /> )} - - ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 34f6c621df..4d9ee5bc98 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -3,19 +3,22 @@ import { type ReactNode, useState } from 'react'; import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { ResizablePanelEdge } from '@/ui/layout/resizable-panel/components/ResizablePanelEdge'; import { NAVIGATION_DRAWER_COLLAPSED_WIDTH } from '@/ui/layout/resizable-panel/constants/NavigationDrawerCollapsedWidth'; import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; +import { NavigationDrawerWidthEffect } from '@/ui/navigation/components/NavigationDrawerWidthEffect'; +import { NAVIGATION_DRAWER_CLICK_OUTSIDE_ID } from '@/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState'; +import { NAVIGATION_DRAWER_TABS } from '@/ui/navigation/states/navigationDrawerTabs'; import { NAVIGATION_DRAWER_WIDTH_VAR, navigationDrawerWidthState, } from '@/ui/navigation/states/navigationDrawerWidthState'; -import { NavigationDrawerWidthEffect } from '@/ui/navigation/components/NavigationDrawerWidthEffect'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerHeader } from './NavigationDrawerHeader'; @@ -47,11 +50,13 @@ const StyledAnimatedContainer = styled.div<{ const StyledContainer = styled.div<{ isSettings?: boolean; isMobile?: boolean; + isExpanded?: boolean; }>` box-sizing: border-box; display: flex; flex-direction: column; - width: var(${NAVIGATION_DRAWER_WIDTH_VAR}); + width: ${({ isExpanded }) => + isExpanded ? `var(${NAVIGATION_DRAWER_WIDTH_VAR})` : '100%'}; gap: ${({ theme }) => theme.spacing(3)}; height: 100%; padding: ${({ theme, isSettings, isMobile }) => @@ -72,7 +77,6 @@ export const NavigationDrawer = ({ className, title, }: NavigationDrawerProps) => { - const [isHovered, setIsHovered] = useState(false); const [isResizing, setIsResizing] = useState(false); const isMobile = useIsMobile(); const isSettingsDrawer = useIsSettingsDrawer(); @@ -82,20 +86,16 @@ export const NavigationDrawer = ({ const [navigationDrawerWidth, setNavigationDrawerWidth] = useAtomState( navigationDrawerWidthState, ); + const setNavigationDrawerActiveTab = useSetAtomState( + navigationDrawerActiveTabState, + ); const setTableWidthResizeIsActive = useSetAtomState( tableWidthResizeIsActiveState, ); - const handleHover = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - const handleCollapse = () => { setIsNavigationDrawerExpanded(false); + setNavigationDrawerActiveTab(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU); setIsResizing(false); setTableWidthResizeIsActive(true); }; @@ -116,21 +116,20 @@ export const NavigationDrawer = ({ {isSettingsDrawer && title ? ( !isMobile && ) : ( - + )} - {children} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx index 2b134d34d8..d4d880f208 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx @@ -1,4 +1,7 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState'; +import { NAVIGATION_DRAWER_TABS } from '@/ui/navigation/states/navigationDrawerTabs'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import styled from '@emotion/styled'; import { @@ -26,26 +29,28 @@ export const NavigationDrawerCollapseButton = ({ className, direction = 'left', }: NavigationDrawerCollapseButtonProps) => { - const setIsNavigationDrawerExpanded = useSetAtomState( - isNavigationDrawerExpandedState, + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useAtomState(isNavigationDrawerExpandedState); + const setNavigationDrawerActiveTab = useSetAtomState( + navigationDrawerActiveTabState, ); + const handleClick = () => { + if (isNavigationDrawerExpanded) { + setNavigationDrawerActiveTab(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU); + } + setIsNavigationDrawerExpanded((previousIsExpanded) => !previousIsExpanded); + }; + return ( - - setIsNavigationDrawerExpanded( - (previousIsExpanded) => !previousIsExpanded, - ) - } - > + diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index 12f3ca1924..5dbfcf00f3 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -1,30 +1,42 @@ 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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ isExpanded: boolean }>` align-items: center; display: flex; + flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')}; + gap: ${({ theme, isExpanded }) => (isExpanded ? 0 : theme.spacing(4))}; user-select: none; padding-right: ${({ theme }) => theme.spacing(2)}; min-height: ${PAGE_BAR_MIN_HEIGHT}px; + transition: gap ${({ theme }) => theme.animation.duration.normal}s ease; +`; + +const StyledRightActions = styled.div<{ isExpanded: boolean }>` + align-items: center; + display: flex; + flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')}; + gap: ${({ theme, isExpanded }) => (isExpanded ? 0 : theme.spacing(1))}; + margin-left: ${({ isExpanded }) => (isExpanded ? 'auto' : 0)}; + transition: gap ${({ theme }) => theme.animation.duration.normal}s ease; `; const StyledNavigationDrawerCollapseButton = styled( NavigationDrawerCollapseButton, -)<{ show?: boolean }>` - height: ${({ theme }) => theme.spacing(4)}; - margin-left: auto; - opacity: ${({ show }) => (show ? 1 : 0)}; +)` + height: ${({ theme }) => theme.spacing(6)}; 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,19 +47,27 @@ export const NavigationDrawerHeader = ({ showCollapseButton, }: NavigationDrawerHeaderProps) => { const isMobile = useIsMobile(); - + const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu(); const isNavigationDrawerExpanded = useAtomStateValue( isNavigationDrawerExpandedState, ); return ( - + - {!isMobile && isNavigationDrawerExpanded && ( - + {!isMobile && ( + + + {isNavigationDrawerExpanded && showCollapseButton && ( + + )} + )} ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index bfa2c20e91..22bf852e32 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -51,6 +51,7 @@ export type NavigationDrawerItemProps = { count?: number; keyboard?: string[]; rightOptions?: ReactNode; + alwaysShowRightOptions?: boolean; isDragging?: boolean; isRightOptionsDropdownOpen?: boolean; triggerEvent?: TriggerEventType; @@ -257,6 +258,7 @@ const visibleStateStyles = css` const StyledRightOptionsVisbility = styled.div<{ isMobile: boolean; isRightOptionsDropdownOpen?: boolean; + alwaysVisible?: boolean; }>` display: block; opacity: 0; @@ -269,8 +271,9 @@ const StyledRightOptionsVisbility = styled.div<{ height: 1px; width: 1px; - ${({ isMobile, isRightOptionsDropdownOpen }) => - (isMobile || isRightOptionsDropdownOpen) && visibleStateStyles} + ${({ isMobile, isRightOptionsDropdownOpen, alwaysVisible }) => + (isMobile || isRightOptionsDropdownOpen || alwaysVisible) && + visibleStateStyles} .navigation-drawer-item:hover & { ${visibleStateStyles} @@ -294,6 +297,7 @@ export const NavigationDrawerItem = ({ keyboard, subItemState, rightOptions, + alwaysShowRightOptions = false, isDragging, isRightOptionsDropdownOpen, triggerEvent, @@ -456,6 +460,7 @@ export const NavigationDrawerItem = ({ }} > ({ + key: 'navigationDrawerActiveTab', + defaultValue: NAVIGATION_DRAWER_TABS.NAVIGATION_MENU, + }); diff --git a/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerTabs.ts b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerTabs.ts new file mode 100644 index 0000000000..6434286406 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerTabs.ts @@ -0,0 +1,7 @@ +export const NAVIGATION_DRAWER_TABS = { + NAVIGATION_MENU: 'home', + AI_CHAT_HISTORY: 'chat', +} as const; + +export type NavigationDrawerActiveTab = + (typeof NAVIGATION_DRAWER_TABS)[keyof typeof NAVIGATION_DRAWER_TABS]; diff --git a/packages/twenty-front/src/utils/__tests__/date-utils.test.ts b/packages/twenty-front/src/utils/__tests__/date-utils.test.ts index 53af6a3b36..c43438cef9 100644 --- a/packages/twenty-front/src/utils/__tests__/date-utils.test.ts +++ b/packages/twenty-front/src/utils/__tests__/date-utils.test.ts @@ -10,6 +10,7 @@ import { beautifyExactDate, beautifyExactDateTime, beautifyPastDateRelativeToNow, + beautifyPastDateRelativeToNowShort, hasDatePassed, parseDate, } from '~/utils/date-utils'; @@ -122,6 +123,66 @@ describe('beautifyPastDateRelativeToNow', () => { }); }); +describe('beautifyPastDateRelativeToNowShort', () => { + it('should return "now" for dates less than 60 seconds ago', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-12-31T23:59:15.000Z', + ); + expect(result).toBe('now'); + }); + + it('should return minutes format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-12-31T23:55:00.000Z', + ); + expect(result).toBe('5m'); + }); + + it('should return hours format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-12-31T22:00:00.000Z', + ); + expect(result).toBe('2h'); + }); + + it('should return days format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-12-29T00:00:00.000Z', + ); + expect(result).toBe('3d'); + }); + + it('should return weeks format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-12-18T00:00:00.000Z', + ); + expect(result).toBe('2w'); + }); + + it('should return months format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2023-08-01T00:00:00.000Z', + ); + expect(result).toBe('5mo'); + }); + + it('should return years format', () => { + const result = beautifyPastDateRelativeToNowShort( + '2022-01-01T00:00:00.000Z', + ); + expect(result).toBe('2y'); + }); + + it('should return empty string and log error for invalid date', () => { + const result = beautifyPastDateRelativeToNowShort('invalid-date-string'); + + expect(logError).toHaveBeenCalledWith( + Error('Invalid date passed to formatPastDate: "invalid-date-string"'), + ); + expect(result).toBe(''); + }); +}); + describe('hasDatePassed', () => { it('should log an error and return false when passed an invalid date string', () => { const result = hasDatePassed('invalid-date-string'); diff --git a/packages/twenty-front/src/utils/date-utils.ts b/packages/twenty-front/src/utils/date-utils.ts index 18ed9da08a..46eb391dd0 100644 --- a/packages/twenty-front/src/utils/date-utils.ts +++ b/packages/twenty-front/src/utils/date-utils.ts @@ -115,6 +115,43 @@ export const beautifyPastDateRelativeToNow = ( } }; +export const beautifyPastDateRelativeToNowShort = ( + pastDate: Date | string | number, +) => { + try { + const parsedDate = parseDate(pastDate); + const now = new Date(); + const diffInSeconds = Math.abs( + (now.getTime() - parsedDate.getTime()) / 1000, + ); + + if (diffInSeconds < 60) return t`now`; + + const diffInMinutes = Math.floor(diffInSeconds / 60); + if (diffInMinutes < 60) return `${diffInMinutes}m`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours}h`; + + const diffInDays = Math.floor(diffInHours / 24); + if (diffInDays < 7) return `${diffInDays}d`; + + const diffInWeeks = Math.floor(diffInDays / 7); + if (diffInWeeks < 5) return `${diffInWeeks}w`; + + const diffInMonths = Math.floor(diffInDays / 30); + if (diffInMonths < 12) return `${diffInMonths}mo`; + + const diffInYears = Math.floor(diffInDays / 365); + + return `${diffInYears}y`; + } catch (error) { + logError(error); + + return ''; + } +}; + export const hasDatePassed = (date: Date | string | number) => { try { const parsedDate = parseDate(date); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1771600000000-fix-ai-entity-timestamps-to-timestamptz.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1771600000000-fix-ai-entity-timestamps-to-timestamptz.ts new file mode 100644 index 0000000000..6a9095998c --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1771600000000-fix-ai-entity-timestamps-to-timestamptz.ts @@ -0,0 +1,49 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class FixAiEntityTimestampsToTimestamptz1771600000000 + implements MigrationInterface +{ + name = 'FixAiEntityTimestampsToTimestamptz1771600000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."agentChatThread" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::timestamptz`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentChatThread" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE USING "updatedAt"::timestamptz`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessage" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::timestamptz`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::timestamptz`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentTurn" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::timestamptz`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentTurnEvaluation" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::timestamptz`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."agentTurnEvaluation" ALTER COLUMN "createdAt" TYPE TIMESTAMP USING "createdAt"::timestamp`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentTurn" ALTER COLUMN "createdAt" TYPE TIMESTAMP USING "createdAt"::timestamp`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ALTER COLUMN "createdAt" TYPE TIMESTAMP USING "createdAt"::timestamp`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessage" ALTER COLUMN "createdAt" TYPE TIMESTAMP USING "createdAt"::timestamp`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentChatThread" ALTER COLUMN "updatedAt" TYPE TIMESTAMP USING "updatedAt"::timestamp`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentChatThread" ALTER COLUMN "createdAt" TYPE TIMESTAMP USING "createdAt"::timestamp`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts index 84fa5917ca..5bf0f78343 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts @@ -93,6 +93,6 @@ export class AgentMessagePartEntity { @Column({ type: 'jsonb', nullable: true }) providerMetadata: Record> | null; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity.ts index 49841b1cab..cf6c4c87f9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity.ts @@ -55,6 +55,6 @@ export class AgentMessageEntity { @OneToMany(() => AgentMessagePartEntity, (part) => part.message) parts: Relation; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity.ts index ef033a75ff..5ef2ba1bc2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-turn.entity.ts @@ -39,6 +39,6 @@ export class AgentTurnEntity { @OneToMany(() => AgentTurnEvaluationEntity, (evaluation) => evaluation.turn) evaluations: Relation; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity.ts index d6eb6c4026..e98d695ccf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-monitor/entities/agent-turn-evaluation.entity.ts @@ -32,6 +32,6 @@ export class AgentTurnEvaluationEntity { @Column({ type: 'text', nullable: true }) comment: string | null; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity.ts index 415489aca7..13925382d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity.ts @@ -57,9 +57,9 @@ export class AgentChatThreadEntity { @OneToMany(() => AgentMessageEntity, (message) => message.thread) messages: EntityRelation; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; - @UpdateDateColumn() + @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; }