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:
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
import { createState } from '@ui/utilities/state/utils/createState';
|
||||
|
||||
export const copilotQueryState = createState({
|
||||
key: 'activities/copilot-query',
|
||||
defaultValue: '',
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user