271 remove is command menu v2 enabled (#10809)

Closes https://github.com/twentyhq/core-team-issues/issues/271

This PR
- Removes the feature flag IS_COMMAND_MENU_V2_ENABLED
- Removes all old Right drawer components
- Removes the Action menu bar
- Removes unused Copilot page
This commit is contained in:
Raphaël Bosi
2025-03-12 16:26:29 +01:00
committed by GitHub
parent 1b0413bf8b
commit daa501549e
124 changed files with 281 additions and 4222 deletions

View File

@ -1,7 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui';
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
@ -9,7 +8,6 @@ import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
const StyledInlineCellBaseContainer = styled.div`
align-items: center;
@ -68,9 +66,6 @@ export const CalendarEventParticipantsResponseStatusField = ({
participants: CalendarEventParticipant[];
}) => {
const theme = useTheme();
const isRightDrawerAnimationCompleted = useRecoilValue(
isRightDrawerAnimationCompletedState,
);
const Icon = {
Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
@ -103,9 +98,7 @@ export const CalendarEventParticipantsResponseStatusField = ({
</StyledLabelContainer>
</StyledLabelAndIconContainer>
<StyledDiv ref={participantsContainerRef}>
{isRightDrawerAnimationCompleted && (
<ExpandableList isChipCountDisplayed>{styledChips}</ExpandableList>
)}
<ExpandableList isChipCountDisplayed>{styledChips}</ExpandableList>
</StyledDiv>
</StyledInlineCellBaseContainer>
</StyledPropertyBox>

View File

@ -6,13 +6,11 @@ import { useRecoilValue } from 'recoil';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import {
Avatar,
@ -24,7 +22,6 @@ import {
} from 'twenty-ui';
import {
CalendarChannelVisibility,
FeatureFlagKey,
TimelineCalendarEvent,
} from '~/generated-metadata/graphql';
@ -117,11 +114,7 @@ export const CalendarEventRow = ({
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { displayCurrentEventCursor = false } = useContext(CalendarContext);
const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();
const { openCalendarEventInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const startsAt = getCalendarEventStartDate(calendarEvent);
const endsAt = getCalendarEventEndDate(calendarEvent);
@ -145,11 +138,7 @@ export const CalendarEventRow = ({
onClick={
showTitle
? () => {
if (isCommandMenuV2Enabled) {
openCalendarEventInCommandMenu(calendarEvent.id);
} else {
openCalendarEventRightDrawer(calendarEvent.id);
}
openCalendarEventInCommandMenu(calendarEvent.id);
}
: undefined
}

View File

@ -1,36 +0,0 @@
import { useRecoilValue } from 'recoil';
import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails';
import { CalendarEventDetailsEffect } from '@/activities/calendar/components/CalendarEventDetailsEffect';
import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calendar/graphql/operation-signatures/FindOneCalendarEventOperationSignature';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
export const RightDrawerCalendarEvent = () => {
const { upsertRecords } = useUpsertRecordsInStore();
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
objectNameSingular:
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
objectRecordId: viewableRecordId ?? '',
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
onCompleted: (record) => upsertRecords([record]),
});
if (!calendarEvent) {
return null;
}
return (
<RecordFieldValueSelectorContextProvider>
<CalendarEventDetailsEffect record={calendarEvent} />
<RecordValueSetterEffect recordId={calendarEvent.id} />
<CalendarEventDetails calendarEvent={calendarEvent} />
</RecordFieldValueSelectorContextProvider>
);
};

View File

@ -1,33 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
describe('useOpenCalendarEventRightDrawer', () => {
it('opens the right drawer with the calendar event', () => {
const { result } = renderHook(
() => {
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return {
...useOpenCalendarEventRightDrawer(),
isRightDrawerOpen,
viewableRecordId,
};
},
{ wrapper: RecoilRoot },
);
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openCalendarEventRightDrawer('1234');
});
expect(result.current.isRightDrawerOpen).toBe(true);
expect(result.current.viewableRecordId).toBe('1234');
});
});

View File

@ -1,25 +0,0 @@
import { useSetRecoilState } from 'recoil';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconCalendarEvent } from 'twenty-ui';
export const useOpenCalendarEventRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const openCalendarEventRightDrawer = (calendarEventId: string) => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewCalendarEvent, {
title: 'Calendar Event',
Icon: IconCalendarEvent,
});
setViewableRecordId(calendarEventId);
};
return { openCalendarEventRightDrawer };
};

View File

@ -1,38 +1,35 @@
import { useApolloClient } from '@apollo/client';
import { PartialBlock } from '@blocknote/core';
import { useCreateBlockNote } from '@blocknote/react';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import { useCallback, useMemo } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared';
import { useDebouncedCallback } from 'use-debounce';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { PartialBlock } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import '@blocknote/react/style.css';
import { FeatureFlagKey } from '~/generated/graphql';
import { isArray, isNonEmptyString } from '@sniptt/guards';
type ActivityRichTextEditorProps = {
activityId: string;
@ -50,10 +47,6 @@ export const ActivityRichTextEditor = ({
const cache = useApolloClient().cache;
const activity = activityInStore as Task | Note | null;
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: activityObjectNameSingular,
@ -280,9 +273,7 @@ export const ActivityRichTextEditor = ({
editor.setTextCursorPosition(newBlockId, 'end');
editor.focus();
},
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[],
{
preventDefault: false,

View File

@ -1,54 +0,0 @@
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState';
import {
AutosizeTextInput,
AutosizeTextInputVariant,
} from '@/ui/input/components/AutosizeTextInput';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
overflow-y: auto;
position: relative;
`;
const StyledChatArea = styled.div`
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
padding: ${({ theme }) => theme.spacing(6)};
padding-bottom: 0px;
`;
const StyledNewMessageArea = styled.div`
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(6)};
padding-top: 0px;
`;
export const RightDrawerAIChat = () => {
const setCopilotQuery = useSetRecoilState(copilotQueryState);
return (
<StyledContainer>
<StyledChatArea>{/* TODO */}</StyledChatArea>
<StyledNewMessageArea>
<AutosizeTextInput
autoFocus
placeholder="Ask anything"
variant={AutosizeTextInputVariant.Icon}
onValidate={(text) => {
setCopilotQuery(text);
}}
/>
</StyledNewMessageArea>
</StyledContainer>
);
};

View File

@ -1,18 +0,0 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconSparkles } from 'twenty-ui';
export const useOpenCopilotRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.Copilot, {
title: 'Copilot',
Icon: IconSparkles,
});
};
};

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const copilotQueryState = createState({
key: 'activities/copilot-query',
defaultValue: '',
});

View File

@ -1,19 +1,10 @@
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { Avatar, GRAY_SCALE } from 'twenty-ui';
import { ActivityRow } from '@/activities/components/ActivityRow';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
FeatureFlagKey,
MessageChannelVisibility,
TimelineThread,
} from '~/generated/graphql';
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const StyledHeading = styled.div<{ unread: boolean }>`
@ -77,11 +68,7 @@ type EmailThreadPreviewProps = {
};
export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
const { openEmailThread } = useEmailThread();
const { openEmailThreadInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const visibility = thread.visibility;
@ -103,48 +90,19 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
false,
];
const { isSameEventThanRightDrawerClose } = useRightDrawer();
const handleThreadClick = () => {
const canOpen =
thread.visibility === MessageChannelVisibility.SHARE_EVERYTHING;
const handleThreadClick = useRecoilCallback(
({ snapshot }) =>
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const clickJustTriggeredEmailDrawerClose =
isSameEventThanRightDrawerClose(event.nativeEvent);
const emailThreadIdWhenEmailThreadWasClosed = snapshot
.getLoadable(emailThreadIdWhenEmailThreadWasClosedState)
.getValue();
const canOpen =
thread.visibility === MessageChannelVisibility.SHARE_EVERYTHING &&
(!clickJustTriggeredEmailDrawerClose ||
emailThreadIdWhenEmailThreadWasClosed !== thread.id);
if (canOpen) {
if (isCommandMenuV2Enabled) {
openEmailThreadInCommandMenu(thread.id);
} else {
openEmailThread(thread.id);
}
}
},
[
isCommandMenuV2Enabled,
isSameEventThanRightDrawerClose,
openEmailThread,
openEmailThreadInCommandMenu,
thread.id,
thread.visibility,
],
);
if (canOpen) {
openEmailThreadInCommandMenu(thread.id);
}
};
const isDisabled = visibility !== MessageChannelVisibility.SHARE_EVERYTHING;
return (
<ActivityRow
onClick={(event) => handleThreadClick(event)}
disabled={isDisabled}
>
<ActivityRow onClick={handleThreadClick} disabled={isDisabled}>
<StyledHeading unread={!thread.read}>
<StyledParticipantsContainer>
<Avatar

View File

@ -1,68 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
const viewableEmailThreadId = '1234';
describe('useEmailThread', () => {
it('should open email thread', () => {
const { result } = renderHook(
() => {
const emailThread = useEmailThread();
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return { ...emailThread, isRightDrawerOpen, viewableRecordId };
},
{ wrapper: RecoilRoot },
);
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openEmailThread(viewableEmailThreadId);
});
expect(result.current.isRightDrawerOpen).toBe(true);
expect(result.current.viewableRecordId).toBe(viewableEmailThreadId);
});
it('should close email thread if trying to open the same thread id', () => {
const { result } = renderHook(
() => {
const emailThread = useEmailThread();
const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
isRightDrawerOpenState,
);
const [viewableRecordId, setViewableRecordId] = useRecoilState(
viewableRecordIdState,
);
return {
...emailThread,
isRightDrawerOpen,
viewableRecordId,
setIsRightDrawerOpen,
setViewableRecordId,
};
},
{ wrapper: RecoilRoot },
);
act(() => {
result.current.setIsRightDrawerOpen(true);
result.current.setViewableRecordId(viewableEmailThreadId);
});
act(() => {
result.current.openEmailThread(viewableEmailThreadId);
});
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
});
});

View File

@ -1,36 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
export const useEmailThread = () => {
const { closeRightDrawer } = useRightDrawer();
const openEmailThreadRightDrawer = useOpenEmailThreadRightDrawer();
const openEmailThread = useRecoilCallback(
({ snapshot, set }) =>
(threadId: string) => {
const isRightDrawerOpen = snapshot
.getLoadable(isRightDrawerOpenState)
.getValue();
const viewableEmailThreadId = snapshot
.getLoadable(viewableRecordIdState)
.getValue();
if (isRightDrawerOpen && viewableEmailThreadId === threadId) {
set(viewableRecordIdState, null);
closeRightDrawer();
return;
}
openEmailThreadRightDrawer();
set(viewableRecordIdState, threadId);
},
[closeRightDrawer, openEmailThreadRightDrawer],
);
return { openEmailThread };
};

View File

@ -1,44 +0,0 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { Button, IconArrowsVertical } from 'twenty-ui';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
const StyledButtonContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
padding: 16px 24px;
`;
export const IntermediaryMessages = ({
messages,
}: {
messages: EmailThreadMessageWithSender[];
}) => {
const [areMessagesOpen, setAreMessagesOpen] = useState(false);
if (messages.length === 0) {
return null;
}
return areMessagesOpen ? (
messages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
/>
))
) : (
<StyledButtonContainer>
<Button
Icon={IconArrowsVertical}
title={`${messages.length} email${messages.length > 1 ? 's' : ''}`}
size="small"
onClick={() => setAreMessagesOpen(true)}
/>
</StyledButtonContainer>
);
};

View File

@ -1,185 +0,0 @@
import styled from '@emotion/styled';
import { useEffect, useMemo } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { ConnectedAccountProvider } from 'twenty-shared';
import { Button, IconArrowBackUp } from 'twenty-ui';
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
height: 85%;
overflow-y: auto;
`;
const StyledButtonContainer = styled.div<{ isMobile: boolean }>`
background: ${({ theme }) => theme.background.secondary};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
height: ${({ isMobile }) => (isMobile ? '100px' : '50px')};
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
box-sizing: border-box;
`;
export const RightDrawerEmailThread = () => {
const setMessageThread = useSetRecoilState(messageThreadState);
const isMobile = useIsMobile();
const {
thread,
messages,
fetchMoreMessages,
threadLoading,
messageThreadExternalId,
connectedAccountHandle,
messageChannelLoading,
connectedAccountProvider,
lastMessageExternalId,
} = useRightDrawerEmailThread();
useEffect(() => {
if (!messages[0]?.messageThread) {
return;
}
setMessageThread(messages[0]?.messageThread);
});
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
);
useRegisterClickOutsideListenerCallback({
callbackId:
'EmailThreadClickOutsideCallBack-' + (thread?.id ?? 'no-thread-id'),
callbackFunction: useRecoilCallback(
({ set }) =>
() => {
set(
emailThreadIdWhenEmailThreadWasClosedState,
thread?.id ?? 'no-thread-id',
);
},
[thread],
),
});
const messagesCount = messages.length;
const is5OrMoreMessages = messagesCount >= 5;
const firstMessages = messages.slice(
0,
is5OrMoreMessages ? 2 : messagesCount - 1,
);
const intermediaryMessages = is5OrMoreMessages
? messages.slice(2, messagesCount - 1)
: [];
const lastMessage = messages[messagesCount - 1];
const subject = messages[0]?.subject;
const canReply = useMemo(() => {
return (
connectedAccountHandle &&
connectedAccountProvider &&
lastMessage &&
messageThreadExternalId != null
);
}, [
connectedAccountHandle,
connectedAccountProvider,
lastMessage,
messageThreadExternalId,
]);
const handleReplyClick = () => {
if (!canReply) {
return;
}
let url: string;
switch (connectedAccountProvider) {
case ConnectedAccountProvider.MICROSOFT:
url = `https://outlook.office.com/mail/deeplink?ItemID=${lastMessageExternalId}`;
window.open(url, '_blank');
break;
case ConnectedAccountProvider.GOOGLE:
url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
window.open(url, '_blank');
break;
case null:
throw new Error('Account provider not provided');
default:
assertUnreachable(connectedAccountProvider);
}
};
if (!thread || !messages.length) {
return null;
}
return (
<StyledWrapper>
<StyledContainer>
{threadLoading ? (
<EmailLoader loadingText="Loading thread" />
) : (
<>
<EmailThreadHeader
subject={subject}
lastMessageSentAt={lastMessage.receivedAt}
/>
{firstMessages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
/>
))}
<IntermediaryMessages messages={intermediaryMessages} />
<EmailThreadMessage
key={lastMessage.id}
sender={lastMessage.sender}
participants={lastMessage.messageParticipants}
body={lastMessage.text}
sentAt={lastMessage.receivedAt}
isExpanded
/>
<CustomResolverFetchMoreLoader
loading={threadLoading}
onLastRowVisible={fetchMoreMessages}
/>
</>
)}
</StyledContainer>
{canReply && !messageChannelLoading && (
<StyledButtonContainer isMobile={isMobile}>
<Button
onClick={handleReplyClick}
title="Reply"
Icon={IconArrowBackUp}
disabled={!canReply}
/>
</StyledButtonContainer>
)}
</StyledWrapper>
);
};

View File

@ -1,40 +0,0 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { IconMail } from 'twenty-ui';
const mockOpenRightDrawer = jest.fn();
const mockSetHotkeyScope = jest.fn();
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
useRightDrawer: () => ({
openRightDrawer: mockOpenRightDrawer,
}),
}));
jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
useSetHotkeyScope: () => mockSetHotkeyScope,
}));
test('useOpenEmailThreadRightDrawer opens the email thread right drawer', () => {
const { result } = renderHook(() => useOpenEmailThreadRightDrawer());
act(() => {
result.current();
});
expect(mockSetHotkeyScope).toHaveBeenCalledWith(
RightDrawerHotkeyScope.RightDrawer,
{ goto: false },
);
expect(mockOpenRightDrawer).toHaveBeenCalledWith(
RightDrawerPages.ViewEmailThread,
{
title: 'Email Thread',
Icon: IconMail,
},
);
});

View File

@ -1,416 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import gql from 'graphql-tag';
import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread';
const mocks = [
{
request: {
query: gql`
query FindOneMessageThread($objectRecordId: ID!) {
messageThread(filter: { id: { eq: $objectRecordId } }) {
__typename
id
}
}
`,
variables: { objectRecordId: '1' },
},
result: jest.fn(() => ({
data: {
messageThread: {
id: '1',
__typename: 'MessageThread',
},
},
})),
},
{
request: {
query: gql`
query FindManyMessages(
$filter: MessageFilterInput
$orderBy: [MessageOrderByInput]
$lastCursor: String
$limit: Int
) {
messages(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
createdAt
headerMessageId
id
messageParticipants {
edges {
node {
__typename
displayName
handle
id
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
role
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
}
}
}
messageThread {
__typename
id
}
receivedAt
subject
text
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: { messageThreadId: { eq: '1' } },
orderBy: [{ receivedAt: 'AscNullsLast' }],
lastCursor: undefined,
limit: 10,
},
},
result: jest.fn(() => ({
data: {
messages: {
edges: [
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'message',
input: {
id: '1',
text: 'Message 1',
createdAt: '2024-10-03T10:20:10.145Z',
},
}),
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'message',
input: {
id: '2',
text: 'Message 2',
createdAt: '2024-10-03T10:20:10.145Z',
},
}),
cursor: '2',
},
],
totalCount: 2,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '1',
endCursor: '2',
},
},
},
})),
},
{
request: {
query: gql`
query FindManyMessageParticipants(
$filter: MessageParticipantFilterInput
$orderBy: [MessageParticipantOrderByInput]
$lastCursor: String
$limit: Int
) {
messageParticipants(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
displayName
handle
id
messageId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
role
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: { messageId: { in: ['1', '2'] }, role: { eq: 'from' } },
orderBy: undefined,
lastCursor: undefined,
limit: undefined,
},
},
result: jest.fn(() => ({
data: {
messageParticipants: {
edges: [
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-1',
role: 'from',
messageId: '1',
},
}),
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-2',
role: 'from',
messageId: '2',
},
}),
cursor: '2',
},
],
totalCount: 2,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '1',
endCursor: '2',
},
},
},
})),
},
];
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks,
onInitializeRecoilSnapshot: ({ set }) => {
set(viewableRecordIdState, '1');
},
});
describe('useRightDrawerEmailThread', () => {
it('should return correct values', async () => {
const mockMessages = [
{
__typename: 'Message',
createdAt: '2024-10-03T10:20:10.145Z',
headerMessageId: '',
id: '1',
messageParticipants: [],
messageThread: null,
receivedAt: null,
sender: {
__typename: 'MessageParticipant',
displayName: '',
handle: '',
id: 'messageParticipant-1',
messageId: '1',
person: null,
role: 'from',
workspaceMember: null,
},
subject: '',
text: 'Message 1',
},
{
__typename: 'Message',
createdAt: '2024-10-03T10:20:10.145Z',
headerMessageId: '',
id: '2',
messageParticipants: [],
messageThread: null,
receivedAt: null,
sender: {
__typename: 'MessageParticipant',
displayName: '',
handle: '',
id: 'messageParticipant-2',
messageId: '2',
person: null,
role: 'from',
workspaceMember: null,
},
subject: '',
text: 'Message 2',
},
];
const { result } = renderHook(() => useRightDrawerEmailThread(), {
wrapper: Wrapper,
});
await waitFor(() => {
expect(result.current.thread).toBeDefined();
expect(result.current.messages).toEqual(mockMessages);
expect(result.current.threadLoading).toBeFalsy();
expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
});
});
});

View File

@ -1,18 +0,0 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconMail } from 'twenty-ui';
export const useOpenEmailThreadRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewEmailThread, {
title: 'Email Thread',
Icon: IconMail,
});
};
};

View File

@ -1,186 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
import { EmailThread } from '@/activities/emails/types/EmailThread';
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
import { MessageChannel } from '@/accounts/types/MessageChannel';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { MessageChannelMessageAssociation } from '@/activities/emails/types/MessageChannelMessageAssociation';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { isDefined } from 'twenty-shared';
export const useRightDrawerEmailThread = () => {
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const { upsertRecords } = useUpsertRecordsInStore();
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
const [lastMessageChannelId, setLastMessageChannelId] = useState<
string | null
>(null);
const [isMessagesFetchComplete, setIsMessagesFetchComplete] = useState(false);
const { record: thread } = useFindOneRecord<EmailThread>({
objectNameSingular: CoreObjectNameSingular.MessageThread,
objectRecordId: viewableRecordId ?? '',
recordGqlFields: {
id: true,
},
onCompleted: (record) => {
upsertRecords([record]);
},
});
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
fetchAllThreadMessagesOperationSignatureFactory({
messageThreadId: viewableRecordId,
});
const {
records: messages,
loading: messagesLoading,
fetchMoreRecords,
hasNextPage,
} = useFindManyRecords<EmailThreadMessage>({
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
objectNameSingular:
FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.objectNameSingular,
orderBy: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.orderBy,
recordGqlFields: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.fields,
skip: !viewableRecordId,
});
const fetchMoreMessages = useCallback(() => {
if (!messagesLoading && hasNextPage) {
fetchMoreRecords();
} else if (!hasNextPage) {
setIsMessagesFetchComplete(true);
}
}, [fetchMoreRecords, messagesLoading, hasNextPage]);
useEffect(() => {
if (messages.length > 0 && isMessagesFetchComplete) {
const lastMessage = messages[messages.length - 1];
setLastMessageId(lastMessage.id);
}
}, [messages, isMessagesFetchComplete]);
// TODO: introduce nested filters so we can retrieve the message sender directly from the message query
const { records: messageSenders } =
useFindManyRecords<EmailThreadMessageParticipant>({
filter: {
messageId: {
in: messages.map(({ id }) => id),
},
role: {
eq: 'from',
},
},
objectNameSingular: CoreObjectNameSingular.MessageParticipant,
recordGqlFields: {
id: true,
role: true,
displayName: true,
messageId: true,
handle: true,
person: true,
workspaceMember: true,
},
skip: messages.length === 0,
});
const { records: messageChannelMessageAssociationData } =
useFindManyRecords<MessageChannelMessageAssociation>({
filter: {
messageId: {
eq: lastMessageId ?? '',
},
},
objectNameSingular:
CoreObjectNameSingular.MessageChannelMessageAssociation,
recordGqlFields: {
id: true,
messageId: true,
messageChannelId: true,
messageThreadExternalId: true,
messageExternalId: true,
},
skip: !lastMessageId || !isMessagesFetchComplete,
});
useEffect(() => {
if (messageChannelMessageAssociationData.length > 0) {
setLastMessageChannelId(
messageChannelMessageAssociationData[0].messageChannelId,
);
}
}, [messageChannelMessageAssociationData]);
const { records: messageChannelData, loading: messageChannelLoading } =
useFindManyRecords<MessageChannel>({
filter: {
id: {
eq: lastMessageChannelId ?? '',
},
},
objectNameSingular: CoreObjectNameSingular.MessageChannel,
recordGqlFields: {
id: true,
handle: true,
connectedAccount: {
id: true,
provider: true,
},
},
skip: !lastMessageChannelId,
});
const messageThreadExternalId =
messageChannelMessageAssociationData.length > 0
? messageChannelMessageAssociationData[0].messageThreadExternalId
: null;
const lastMessageExternalId =
messageChannelMessageAssociationData.length > 0
? messageChannelMessageAssociationData[0].messageExternalId
: null;
const connectedAccountHandle =
messageChannelData.length > 0 ? messageChannelData[0].handle : null;
const messagesWithSender: EmailThreadMessageWithSender[] = messages
.map((message) => {
const sender = messageSenders.find(
(messageSender) => messageSender.messageId === message.id,
);
if (!sender) {
return null;
}
return {
...message,
sender,
};
})
.filter(isDefined);
const connectedAccount =
messageChannelData.length > 0
? messageChannelData[0]?.connectedAccount
: null;
const connectedAccountProvider = connectedAccount?.provider ?? null;
return {
thread,
messages: messagesWithSender,
messageThreadExternalId,
connectedAccountHandle,
connectedAccountProvider,
threadLoading: messagesLoading,
messageChannelLoading,
lastMessageExternalId,
fetchMoreMessages,
};
};

View File

@ -1,40 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider addTypename={false}>{children}</MockedProvider>
</RecoilRoot>
);
describe('useOpenActivityRightDrawer', () => {
it('works as expected', () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return {
openActivityRightDrawer,
viewableRecordId,
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openActivityRightDrawer('123');
});
expect(result.current.viewableRecordId).toBe('123');
});
});

View File

@ -1,63 +0,0 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { IconList } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const useOpenActivityRightDrawer = ({
objectNameSingular,
}: {
objectNameSingular: CoreObjectNameSingular;
}) => {
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
useRightDrawer();
const [viewableRecordId, setViewableRecordId] = useRecoilState(
viewableRecordIdState,
);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const setHotkeyScope = useSetHotkeyScope();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { openRecordInCommandMenu } = useCommandMenu();
return (activityId: string) => {
if (
isRightDrawerOpen &&
rightDrawerPage === RightDrawerPages.ViewRecord &&
viewableRecordId === activityId
) {
return;
}
if (isCommandMenuV2Enabled) {
openRecordInCommandMenu({
recordId: activityId,
objectNameSingular,
isNewRecord: false,
});
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableRecordId(activityId);
setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord, {
title: objectNameSingular,
Icon: IconList,
});
}
};
};

View File

@ -1,27 +1,20 @@
import { useSetRecoilState } from 'recoil';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { IconList } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawer = ({
activityObjectNameSingular,
@ -30,10 +23,6 @@ export const useOpenCreateActivityDrawer = ({
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
}) => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const { createOneRecord: createOneActivity } = useCreateOneRecord<
(Task | Note) & { position: 'first' | 'last' }
>({
@ -64,10 +53,6 @@ export const useOpenCreateActivityDrawer = ({
isUpsertingActivityInDBState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { openRecordInCommandMenu } = useCommandMenu();
const openCreateActivityDrawer = async ({
@ -78,12 +63,6 @@ export const useOpenCreateActivityDrawer = ({
customAssignee?: WorkspaceMember;
}) => {
setIsNewViewableRecordLoading(true);
if (!isCommandMenuV2Enabled) {
openRightDrawer(RightDrawerPages.ViewRecord, {
title: activityObjectNameSingular,
Icon: IconList,
});
}
setViewableRecordId(null);
setViewableRecordNameSingular(activityObjectNameSingular);
@ -125,15 +104,11 @@ export const useOpenCreateActivityDrawer = ({
setActivityTargetableEntityArray([]);
}
if (isCommandMenuV2Enabled) {
openRecordInCommandMenu({
recordId: activity.id,
objectNameSingular: activityObjectNameSingular,
isNewRecord: true,
});
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
}
openRecordInCommandMenu({
recordId: activity.id,
objectNameSingular: activityObjectNameSingular,
isNewRecord: true,
});
setViewableRecordId(activity.id);

View File

@ -5,8 +5,6 @@ import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-pic
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilCallback } from 'recoil';
type OpenActivityTargetInlineCellEditModeProps = {
@ -15,9 +13,6 @@ type OpenActivityTargetInlineCellEditModeProps = {
};
export const useOpenActivityTargetInlineCellEditMode = () => {
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch();
@ -66,8 +61,6 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
'',
);
toggleRightDrawerClickOustideListener(false);
multipleRecordPickerPerformSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId,
forceSearchFilter: '',
@ -83,7 +76,7 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
),
});
},
[multipleRecordPickerPerformSearch, toggleRightDrawerClickOustideListener],
[multipleRecordPickerPerformSearch],
);
return { openActivityTargetInlineCellEditMode };

View File

@ -1,9 +1,9 @@
import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
@ -68,9 +68,7 @@ export const NoteCard = ({
note: Note;
isSingleNote: boolean;
}) => {
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Note,
});
const { openRecordInCommandMenu } = useCommandMenu();
const body = getActivityPreview(note?.bodyV2?.blocknote ?? null);
@ -84,7 +82,12 @@ export const NoteCard = ({
return (
<StyledCard isSingleNote={isSingleNote}>
<StyledCardDetailsContainer
onClick={() => openActivityRightDrawer(note.id)}
onClick={() =>
openRecordInCommandMenu({
recordId: note.id,
objectNameSingular: CoreObjectNameSingular.Note,
})
}
>
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent>

View File

@ -7,13 +7,13 @@ import {
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { ActivityRow } from '@/activities/components/ActivityRow';
import { Task } from '@/activities/types/Task';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useCompleteTask } from '../hooks/useCompleteTask';
@ -78,9 +78,7 @@ const StyledCheckboxContainer = styled.div`
export const TaskRow = ({ task }: { task: Task }) => {
const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const { openRecordInCommandMenu } = useCommandMenu();
const body = getActivitySummary(task?.bodyV2?.blocknote ?? null);
@ -96,7 +94,10 @@ export const TaskRow = ({ task }: { task: Task }) => {
return (
<ActivityRow
onClick={() => {
openActivityRightDrawer(task.id);
openRecordInCommandMenu({
recordId: task.id,
objectNameSingular: CoreObjectNameSingular.Task,
});
}}
>
<StyledLeftSideContainer>

View File

@ -1,11 +1,11 @@
import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import {
EventRowDynamicComponentProps,
StyledEventRowItemAction,
StyledEventRowItemColumn,
} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isNonEmptyString } from '@sniptt/guards';
@ -55,9 +55,7 @@ export const EventRowActivity = ({
? event.linkedRecordCachedName
: 'Untitled';
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular,
});
const { openRecordInCommandMenu } = useCommandMenu();
return (
<>
@ -66,7 +64,12 @@ export const EventRowActivity = ({
{`${eventAction} a related ${eventObject}`}
</StyledEventRowItemAction>
<StyledLinkedActivity
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
onClick={() =>
openRecordInCommandMenu({
recordId: event.linkedRecordId,
objectNameSingular,
})
}
>
{activityTitle}
</StyledLinkedActivity>