feat(server): derive email/calendar timelines from object relations (#21684)
Simplifies our existing implementation that uses three different GraphQL
endpoints to just one `getTimelineEventsFrom{Person, Company,
Opportunity}Id` to `getTimelineCalendarEventsFromObjectRecord`
/closes https://github.com/twentyhq/twenty/issues/19676
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21684?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+4
-23
@@ -6,14 +6,11 @@ import { format, getYear } from 'date-fns';
|
||||
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
|
||||
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from '@/activities/calendar/constants/Calendar';
|
||||
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
|
||||
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId';
|
||||
import { getTimelineCalendarEventsFromOpportunityId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromOpportunityId';
|
||||
import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId';
|
||||
import { getTimelineCalendarEventsFromObjectRecord } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromObjectRecord';
|
||||
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
|
||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
|
||||
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { H3Title } from 'twenty-ui/typography';
|
||||
import {
|
||||
@@ -52,33 +49,17 @@ export const CalendarEventsCard = () => {
|
||||
const targetRecord = useTargetRecord();
|
||||
const { localeCatalog } = useAtomStateValue(dateLocaleState);
|
||||
|
||||
const [query, queryName] =
|
||||
targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Person
|
||||
? [
|
||||
getTimelineCalendarEventsFromPersonId,
|
||||
'getTimelineCalendarEventsFromPersonId',
|
||||
]
|
||||
: targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Company
|
||||
? [
|
||||
getTimelineCalendarEventsFromCompanyId,
|
||||
'getTimelineCalendarEventsFromCompanyId',
|
||||
]
|
||||
: [
|
||||
getTimelineCalendarEventsFromOpportunityId,
|
||||
'getTimelineCalendarEventsFromOpportunityId',
|
||||
];
|
||||
|
||||
const { data, firstQueryLoading, isFetchingMore, fetchMoreRecords } =
|
||||
useCustomResolver<TimelineCalendarEventsWithTotal>(
|
||||
query,
|
||||
queryName,
|
||||
getTimelineCalendarEventsFromObjectRecord,
|
||||
'getTimelineCalendarEventsFromObjectRecord',
|
||||
'timelineCalendarEvents',
|
||||
targetRecord,
|
||||
TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
|
||||
);
|
||||
|
||||
const { timelineCalendarEvents, totalNumberOfCalendarEvents } =
|
||||
data?.[queryName] ?? {};
|
||||
data?.getTimelineCalendarEventsFromObjectRecord ?? {};
|
||||
|
||||
const {
|
||||
calendarEventsByDayTime,
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
|
||||
import { CalendarEventsCard } from '@/activities/calendar/components/CalendarEventsCard';
|
||||
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId';
|
||||
import { getTimelineCalendarEventsFromObjectRecord } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromObjectRecord';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
@@ -41,12 +41,12 @@ const meta: Meta<typeof CalendarEventsCard> = {
|
||||
handlers: [
|
||||
...graphqlMocks.handlers,
|
||||
graphql.query(
|
||||
getOperationName(getTimelineCalendarEventsFromCompanyId) ?? '',
|
||||
getOperationName(getTimelineCalendarEventsFromObjectRecord) ?? '',
|
||||
({ variables }) => {
|
||||
if (variables.page > 1) {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getTimelineCalendarEventsFromCompanyId: {
|
||||
getTimelineCalendarEventsFromObjectRecord: {
|
||||
__typename: 'TimelineCalendarEventsWithTotal',
|
||||
totalNumberOfCalendarEvents: 3,
|
||||
timelineCalendarEvents: [],
|
||||
@@ -56,7 +56,7 @@ const meta: Meta<typeof CalendarEventsCard> = {
|
||||
}
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getTimelineCalendarEventsFromCompanyId: {
|
||||
getTimelineCalendarEventsFromObjectRecord: {
|
||||
__typename: 'TimelineCalendarEventsWithTotal',
|
||||
totalNumberOfCalendarEvents: 3,
|
||||
timelineCalendarEvents: mockedTimelineCalendarEvents,
|
||||
|
||||
+7
-5
@@ -1,14 +1,16 @@
|
||||
import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const getTimelineCalendarEventsFromCompanyId = gql`
|
||||
query GetTimelineCalendarEventsFromCompanyId(
|
||||
$companyId: UUID!
|
||||
export const getTimelineCalendarEventsFromObjectRecord = gql`
|
||||
query GetTimelineCalendarEventsFromObjectRecord(
|
||||
$objectNameSingular: String!
|
||||
$recordId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineCalendarEventsFromCompanyId(
|
||||
companyId: $companyId
|
||||
getTimelineCalendarEventsFromObjectRecord(
|
||||
objectNameSingular: $objectNameSingular
|
||||
recordId: $recordId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment';
|
||||
|
||||
export const getTimelineCalendarEventsFromOpportunityId = gql`
|
||||
query GetTimelineCalendarEventsFromOpportunityId(
|
||||
$opportunityId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineCalendarEventsFromOpportunityId(
|
||||
opportunityId: $opportunityId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
...TimelineCalendarEventsWithTotalFragment
|
||||
}
|
||||
}
|
||||
${timelineCalendarEventWithTotalFragment}
|
||||
`;
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const getTimelineCalendarEventsFromPersonId = gql`
|
||||
query GetTimelineCalendarEventsFromPersonId(
|
||||
$personId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineCalendarEventsFromPersonId(
|
||||
personId: $personId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
...TimelineCalendarEventsWithTotalFragment
|
||||
}
|
||||
}
|
||||
${timelineCalendarEventWithTotalFragment}
|
||||
`;
|
||||
@@ -7,11 +7,8 @@ import { ComposeEmailButton } from '@/activities/emails/components/ComposeEmailB
|
||||
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
|
||||
import { EmptyInboxPlaceholder } from '@/activities/emails/components/EmptyInboxPlaceholder';
|
||||
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging';
|
||||
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId';
|
||||
import { getTimelineThreadsFromOpportunityId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId';
|
||||
import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
|
||||
import { getTimelineThreadsFromObjectRecord } from '@/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord';
|
||||
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { H1Title, H1TitleFontColor } from 'twenty-ui/typography';
|
||||
@@ -52,26 +49,17 @@ const StyledEmailCount = styled.span`
|
||||
export const EmailsCard = () => {
|
||||
const targetRecord = useTargetRecord();
|
||||
|
||||
const [query, queryName] =
|
||||
targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Person
|
||||
? [getTimelineThreadsFromPersonId, 'getTimelineThreadsFromPersonId']
|
||||
: targetRecord.targetObjectNameSingular === CoreObjectNameSingular.Company
|
||||
? [getTimelineThreadsFromCompanyId, 'getTimelineThreadsFromCompanyId']
|
||||
: [
|
||||
getTimelineThreadsFromOpportunityId,
|
||||
'getTimelineThreadsFromOpportunityId',
|
||||
];
|
||||
|
||||
const { data, firstQueryLoading, isFetchingMore, fetchMoreRecords } =
|
||||
useCustomResolver<TimelineThreadsWithTotal>(
|
||||
query,
|
||||
queryName,
|
||||
getTimelineThreadsFromObjectRecord,
|
||||
'getTimelineThreadsFromObjectRecord',
|
||||
'timelineThreads',
|
||||
targetRecord,
|
||||
TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||
);
|
||||
|
||||
const { totalNumberOfThreads, timelineThreads } = data?.[queryName] ?? {};
|
||||
const { totalNumberOfThreads, timelineThreads } =
|
||||
data?.getTimelineThreadsFromObjectRecord ?? {};
|
||||
const hasMoreTimelineThreads =
|
||||
timelineThreads && totalNumberOfThreads
|
||||
? timelineThreads?.length < totalNumberOfThreads
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
gql: jest.fn().mockImplementation((strings) => {
|
||||
return strings.map((str: string) => str.trim()).join(' ');
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('getTimelineThreadsFromCompanyId query', () => {
|
||||
test('should construct the query correctly', () => {
|
||||
expect(gql).toHaveBeenCalled();
|
||||
expect(getTimelineThreadsFromCompanyId).toBeDefined();
|
||||
});
|
||||
});
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
gql: jest.fn().mockImplementation((strings) => {
|
||||
return strings.map((str: string) => str.trim()).join(' ');
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('getTimelineThreadsFromPersonId query', () => {
|
||||
test('should construct the query correctly', () => {
|
||||
expect(gql).toHaveBeenCalled();
|
||||
expect(getTimelineThreadsFromPersonId).toBeDefined();
|
||||
});
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const getTimelineThreadsFromCompanyId = gql`
|
||||
query GetTimelineThreadsFromCompanyId(
|
||||
$companyId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineThreadsFromCompanyId(
|
||||
companyId: $companyId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
...TimelineThreadsWithTotalFragment
|
||||
}
|
||||
}
|
||||
${timelineThreadWithTotalFragment}
|
||||
`;
|
||||
+8
-7
@@ -1,15 +1,16 @@
|
||||
import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment';
|
||||
|
||||
export const getTimelineThreadsFromOpportunityId = gql`
|
||||
query GetTimelineThreadsFromOpportunityId(
|
||||
$opportunityId: UUID!
|
||||
export const getTimelineThreadsFromObjectRecord = gql`
|
||||
query GetTimelineThreadsFromObjectRecord(
|
||||
$objectNameSingular: String!
|
||||
$recordId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineThreadsFromOpportunityId(
|
||||
opportunityId: $opportunityId
|
||||
getTimelineThreadsFromObjectRecord(
|
||||
objectNameSingular: $objectNameSingular
|
||||
recordId: $recordId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment';
|
||||
|
||||
export const getTimelineThreadsFromPersonId = gql`
|
||||
query GetTimelineThreadsFromPersonId(
|
||||
$personId: UUID!
|
||||
$page: Int!
|
||||
$pageSize: Int!
|
||||
) {
|
||||
getTimelineThreadsFromPersonId(
|
||||
personId: $personId
|
||||
page: $page
|
||||
pageSize: $pageSize
|
||||
) {
|
||||
...TimelineThreadsWithTotalFragment
|
||||
}
|
||||
}
|
||||
${timelineThreadWithTotalFragment}
|
||||
`;
|
||||
@@ -3,9 +3,7 @@ import { useCallback } from 'react';
|
||||
import { type EmailAttachment } from 'twenty-shared/types';
|
||||
|
||||
import { SEND_EMAIL } from '@/activities/emails/graphql/mutations/sendEmail';
|
||||
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId';
|
||||
import { getTimelineThreadsFromOpportunityId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId';
|
||||
import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
|
||||
import { getTimelineThreadsFromObjectRecord } from '@/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -60,9 +58,7 @@ export const useSendEmail = () => {
|
||||
|
||||
await apolloCoreClient.refetchQueries({
|
||||
include: [
|
||||
getTimelineThreadsFromCompanyId,
|
||||
getTimelineThreadsFromPersonId,
|
||||
getTimelineThreadsFromOpportunityId,
|
||||
getTimelineThreadsFromObjectRecord,
|
||||
'FindManyMessages',
|
||||
'FindManyMessageParticipants',
|
||||
'FindManyMessageChannelMessageAssociations',
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { getTimelineThreadsFromObjectRecord } from '@/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord';
|
||||
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
|
||||
|
||||
jest.mock('@apollo/client/react', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/object-metadata/hooks/useApolloCoreClient', () => ({
|
||||
useApolloCoreClient: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
jest.mock('@/apollo/hooks/useSnackBarOnQueryError', () => ({
|
||||
useSnackBarOnQueryError: jest.fn(),
|
||||
}));
|
||||
|
||||
const useQueryMock = jest.requireMock('@apollo/client/react').useQuery;
|
||||
|
||||
describe('useCustomResolver', () => {
|
||||
beforeEach(() => {
|
||||
useQueryMock.mockReturnValue({
|
||||
data: undefined,
|
||||
loading: false,
|
||||
fetchMore: jest.fn(),
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('queries the timeline resolver by object name and record id for any object', () => {
|
||||
renderHook(() =>
|
||||
useCustomResolver(
|
||||
getTimelineThreadsFromObjectRecord,
|
||||
'getTimelineThreadsFromObjectRecord',
|
||||
'timelineThreads',
|
||||
{ id: 'record-id', targetObjectNameSingular: 'peopleList' },
|
||||
10,
|
||||
),
|
||||
);
|
||||
|
||||
expect(useQueryMock).toHaveBeenCalledWith(
|
||||
getTimelineThreadsFromObjectRecord,
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
objectNameSingular: 'peopleList',
|
||||
recordId: 'record-id',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import { useState } from 'react';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
|
||||
type CustomResolverQueryResult<
|
||||
T extends {
|
||||
@@ -47,13 +46,8 @@ export const useCustomResolver = <
|
||||
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
||||
|
||||
const queryVariables = {
|
||||
...(activityTargetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Person
|
||||
? { personId: activityTargetableObject.id }
|
||||
: activityTargetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Opportunity
|
||||
? { opportunityId: activityTargetableObject.id }
|
||||
: { companyId: activityTargetableObject.id }),
|
||||
objectNameSingular: activityTargetableObject.targetObjectNameSingular,
|
||||
recordId: activityTargetableObject.id,
|
||||
page: 1,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user