From 814b43ca41a69a4adbece0845a4aedec75708593 Mon Sep 17 00:00:00 2001
From: neo773 <62795688+neo773@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:52:27 +0530
Subject: [PATCH] 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
---------
Co-authored-by: Charles Bochet
---
.../twenty-front/src/generated/graphql.ts | 82 ++--
.../components/CalendarEventsCard.tsx | 27 +-
.../__stories__/Calendar.stories.tsx | 8 +-
...TimelineCalendarEventsFromObjectRecord.ts} | 12 +-
...TimelineCalendarEventsFromOpportunityId.ts | 20 -
.../getTimelineCalendarEventsFromPersonId.ts | 19 -
.../emails/components/EmailsCard.tsx | 22 +-
.../getTimelineThreadsFromCompanyId.test.ts | 16 -
.../getTimelineThreadsFromPersonId.test.ts | 16 -
.../getTimelineThreadsFromCompanyId.ts | 19 -
... => getTimelineThreadsFromObjectRecord.ts} | 15 +-
.../queries/getTimelineThreadsFromPersonId.ts | 20 -
.../activities/emails/hooks/useSendEmail.ts | 8 +-
.../__tests__/useCustomResolver.test.tsx | 57 +++
.../activities/hooks/useCustomResolver.ts | 10 +-
.../timeline-calendar-event.module.ts | 2 +
.../timeline-calendar-event.resolver.ts | 105 +++--
.../timeline-calendar-event.service.spec.ts | 5 +
.../timeline-calendar-event.service.ts | 123 ++----
.../services/get-messages.service.ts | 114 +----
.../messaging/timeline-messaging.module.ts | 2 +
.../messaging/timeline-messaging.resolver.ts | 112 +++--
.../related-person-ids.module.ts | 11 +
.../services/related-person-ids.service.ts | 130 ++++++
...find-relation-paths-to-person.util.spec.ts | 294 +++++++++++++
.../find-relation-paths-to-person.util.ts | 125 ++++++
...ine-from-object-record.integration-spec.ts | 396 ++++++++++++++++++
27 files changed, 1286 insertions(+), 484 deletions(-)
rename packages/twenty-front/src/modules/activities/calendar/graphql/queries/{getTimelineCalendarEventsFromCompanyId.ts => getTimelineCalendarEventsFromObjectRecord.ts} (57%)
delete mode 100644 packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromOpportunityId.ts
delete mode 100644 packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts
delete mode 100644 packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts
delete mode 100644 packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts
delete mode 100644 packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts
rename packages/twenty-front/src/modules/activities/emails/graphql/queries/{getTimelineThreadsFromOpportunityId.ts => getTimelineThreadsFromObjectRecord.ts} (57%)
delete mode 100644 packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts
create mode 100644 packages/twenty-front/src/modules/activities/hooks/__tests__/useCustomResolver.test.tsx
create mode 100644 packages/twenty-server/src/engine/core-modules/related-person-ids/related-person-ids.module.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/related-person-ids/services/related-person-ids.service.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/related-person-ids/utils/__tests__/find-relation-paths-to-person.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util.ts
create mode 100644 packages/twenty-server/test/integration/graphql/suites/timeline-from-object-record.integration-spec.ts
diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts
index 111e7a1751..11ff685dec 100644
--- a/packages/twenty-front/src/generated/graphql.ts
+++ b/packages/twenty-front/src/generated/graphql.ts
@@ -259,11 +259,19 @@ export type ObjectRecordFilterInput = {
export type Query = {
__typename?: 'Query';
+ /** @deprecated Use getTimelineCalendarEventsFromObjectRecord instead */
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
+ getTimelineCalendarEventsFromObjectRecord: TimelineCalendarEventsWithTotal;
+ /** @deprecated Use getTimelineCalendarEventsFromObjectRecord instead */
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
+ /** @deprecated Use getTimelineCalendarEventsFromObjectRecord instead */
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
+ /** @deprecated Use getTimelineThreadsFromObjectRecord instead */
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
+ getTimelineThreadsFromObjectRecord: TimelineThreadsWithTotal;
+ /** @deprecated Use getTimelineThreadsFromObjectRecord instead */
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
+ /** @deprecated Use getTimelineThreadsFromObjectRecord instead */
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
isMaintenanceModeBannerDismissed: Scalars['Boolean']['output'];
search: SearchResultConnection;
@@ -278,6 +286,14 @@ export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
};
+export type QueryGetTimelineCalendarEventsFromObjectRecordArgs = {
+ objectNameSingular: Scalars['String']['input'];
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+ recordId: Scalars['UUID']['input'];
+};
+
+
export type QueryGetTimelineCalendarEventsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID']['input'];
page: Scalars['Int']['input'];
@@ -299,6 +315,14 @@ export type QueryGetTimelineThreadsFromCompanyIdArgs = {
};
+export type QueryGetTimelineThreadsFromObjectRecordArgs = {
+ objectNameSingular: Scalars['String']['input'];
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+ recordId: Scalars['UUID']['input'];
+};
+
+
export type QueryGetTimelineThreadsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID']['input'];
page: Scalars['Int']['input'];
@@ -594,32 +618,15 @@ export type TimelineCalendarEventParticipantFragmentFragment = { __typename?: 'T
export type TimelineCalendarEventsWithTotalFragmentFragment = { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };
-export type GetTimelineCalendarEventsFromCompanyIdQueryVariables = Exact<{
- companyId: Scalars['UUID']['input'];
+export type GetTimelineCalendarEventsFromObjectRecordQueryVariables = Exact<{
+ objectNameSingular: Scalars['String']['input'];
+ recordId: Scalars['UUID']['input'];
page: Scalars['Int']['input'];
pageSize: Scalars['Int']['input'];
}>;
-export type GetTimelineCalendarEventsFromCompanyIdQuery = { __typename?: 'Query', getTimelineCalendarEventsFromCompanyId: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
-
-export type GetTimelineCalendarEventsFromOpportunityIdQueryVariables = Exact<{
- opportunityId: Scalars['UUID']['input'];
- page: Scalars['Int']['input'];
- pageSize: Scalars['Int']['input'];
-}>;
-
-
-export type GetTimelineCalendarEventsFromOpportunityIdQuery = { __typename?: 'Query', getTimelineCalendarEventsFromOpportunityId: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
-
-export type GetTimelineCalendarEventsFromPersonIdQueryVariables = Exact<{
- personId: Scalars['UUID']['input'];
- page: Scalars['Int']['input'];
- pageSize: Scalars['Int']['input'];
-}>;
-
-
-export type GetTimelineCalendarEventsFromPersonIdQuery = { __typename?: 'Query', getTimelineCalendarEventsFromPersonId: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
+export type GetTimelineCalendarEventsFromObjectRecordQuery = { __typename?: 'Query', getTimelineCalendarEventsFromObjectRecord: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
export type ParticipantFragmentFragment = { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
@@ -627,32 +634,15 @@ export type TimelineThreadFragmentFragment = { __typename?: 'TimelineThread', id
export type TimelineThreadsWithTotalFragmentFragment = { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };
-export type GetTimelineThreadsFromCompanyIdQueryVariables = Exact<{
- companyId: Scalars['UUID']['input'];
+export type GetTimelineThreadsFromObjectRecordQueryVariables = Exact<{
+ objectNameSingular: Scalars['String']['input'];
+ recordId: Scalars['UUID']['input'];
page: Scalars['Int']['input'];
pageSize: Scalars['Int']['input'];
}>;
-export type GetTimelineThreadsFromCompanyIdQuery = { __typename?: 'Query', getTimelineThreadsFromCompanyId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
-
-export type GetTimelineThreadsFromOpportunityIdQueryVariables = Exact<{
- opportunityId: Scalars['UUID']['input'];
- page: Scalars['Int']['input'];
- pageSize: Scalars['Int']['input'];
-}>;
-
-
-export type GetTimelineThreadsFromOpportunityIdQuery = { __typename?: 'Query', getTimelineThreadsFromOpportunityId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
-
-export type GetTimelineThreadsFromPersonIdQueryVariables = Exact<{
- personId: Scalars['UUID']['input'];
- page: Scalars['Int']['input'];
- pageSize: Scalars['Int']['input'];
-}>;
-
-
-export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
+export type GetTimelineThreadsFromObjectRecordQuery = { __typename?: 'Query', getTimelineThreadsFromObjectRecord: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
export type SearchQueryVariables = Exact<{
searchInput: Scalars['String']['input'];
@@ -808,12 +798,8 @@ export const ParticipantFragmentFragmentDoc = {"kind":"Document","definitions":[
export const TimelineThreadFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}}]} as unknown as DocumentNode;
export const TimelineThreadsWithTotalFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfThreads"}},{"kind":"Field","name":{"kind":"Name","value":"timelineThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}}]} as unknown as DocumentNode;
export const WorkflowDiffFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkflowDiffFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkflowVersionStepChanges"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerDiff"}},{"kind":"Field","name":{"kind":"Name","value":"stepsDiff"}}]}}]} as unknown as DocumentNode;
-export const GetTimelineCalendarEventsFromCompanyIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineCalendarEventsFromCompanyId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"companyId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineCalendarEventsFromCompanyId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"companyId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"companyId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"startsAt"}},{"kind":"Field","name":{"kind":"Name","value":"endsAt"}},{"kind":"Field","name":{"kind":"Name","value":"isFullDay"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfCalendarEvents"}},{"kind":"Field","name":{"kind":"Name","value":"timelineCalendarEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventFragment"}}]}}]}}]} as unknown as DocumentNode;
-export const GetTimelineCalendarEventsFromOpportunityIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineCalendarEventsFromOpportunityId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"opportunityId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineCalendarEventsFromOpportunityId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"opportunityId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"opportunityId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"startsAt"}},{"kind":"Field","name":{"kind":"Name","value":"endsAt"}},{"kind":"Field","name":{"kind":"Name","value":"isFullDay"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfCalendarEvents"}},{"kind":"Field","name":{"kind":"Name","value":"timelineCalendarEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventFragment"}}]}}]}}]} as unknown as DocumentNode;
-export const GetTimelineCalendarEventsFromPersonIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineCalendarEventsFromPersonId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineCalendarEventsFromPersonId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"startsAt"}},{"kind":"Field","name":{"kind":"Name","value":"endsAt"}},{"kind":"Field","name":{"kind":"Name","value":"isFullDay"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfCalendarEvents"}},{"kind":"Field","name":{"kind":"Name","value":"timelineCalendarEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventFragment"}}]}}]}}]} as unknown as DocumentNode;
-export const GetTimelineThreadsFromCompanyIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineThreadsFromCompanyId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"companyId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineThreadsFromCompanyId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"companyId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"companyId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfThreads"}},{"kind":"Field","name":{"kind":"Name","value":"timelineThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadFragment"}}]}}]}}]} as unknown as DocumentNode;
-export const GetTimelineThreadsFromOpportunityIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineThreadsFromOpportunityId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"opportunityId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineThreadsFromOpportunityId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"opportunityId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"opportunityId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfThreads"}},{"kind":"Field","name":{"kind":"Name","value":"timelineThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadFragment"}}]}}]}}]} as unknown as DocumentNode;
-export const GetTimelineThreadsFromPersonIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineThreadsFromPersonId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineThreadsFromPersonId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfThreads"}},{"kind":"Field","name":{"kind":"Name","value":"timelineThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadFragment"}}]}}]}}]} as unknown as DocumentNode;
+export const GetTimelineCalendarEventsFromObjectRecordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineCalendarEventsFromObjectRecord"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectNameSingular"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recordId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineCalendarEventsFromObjectRecord"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"objectNameSingular"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectNameSingular"}}},{"kind":"Argument","name":{"kind":"Name","value":"recordId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recordId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"startsAt"}},{"kind":"Field","name":{"kind":"Name","value":"endsAt"}},{"kind":"Field","name":{"kind":"Name","value":"isFullDay"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfCalendarEvents"}},{"kind":"Field","name":{"kind":"Name","value":"timelineCalendarEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventFragment"}}]}}]}}]} as unknown as DocumentNode;
+export const GetTimelineThreadsFromObjectRecordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTimelineThreadsFromObjectRecord"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectNameSingular"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recordId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getTimelineThreadsFromObjectRecord"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"objectNameSingular"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectNameSingular"}}},{"kind":"Argument","name":{"kind":"Name","value":"recordId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recordId"}}},{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThread"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"read"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"firstParticipant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastTwoParticipants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageReceivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastMessageBody"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"numberOfMessagesInThread"}},{"kind":"Field","name":{"kind":"Name","value":"participantCount"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineThreadsWithTotalFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineThreadsWithTotal"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalNumberOfThreads"}},{"kind":"Field","name":{"kind":"Name","value":"timelineThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineThreadFragment"}}]}}]}}]} as unknown as DocumentNode;
export const SearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Search"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"searchInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"excludedObjectNameSingulars"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"includedObjectNameSingulars"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectRecordFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"search"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"searchInput"},"value":{"kind":"Variable","name":{"kind":"Name","value":"searchInput"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"excludedObjectNameSingulars"},"value":{"kind":"Variable","name":{"kind":"Name","value":"excludedObjectNameSingulars"}}},{"kind":"Argument","name":{"kind":"Name","value":"includedObjectNameSingulars"},"value":{"kind":"Variable","name":{"kind":"Name","value":"includedObjectNameSingulars"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectNameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"objectLabelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"tsRankCD"}},{"kind":"Field","name":{"kind":"Name","value":"tsRank"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ActivateWorkflowVersionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ActivateWorkflowVersion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workflowVersionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activateWorkflowVersion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workflowVersionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workflowVersionId"}}}]}]}}]} as unknown as DocumentNode;
export const ComputeStepOutputSchemaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ComputeStepOutputSchema"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ComputeStepOutputSchemaInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"computeStepOutputSchema"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode;
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
index c40a4b8e73..6b8ee007cf 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventsCard.tsx
@@ -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(
- query,
- queryName,
+ getTimelineCalendarEventsFromObjectRecord,
+ 'getTimelineCalendarEventsFromObjectRecord',
'timelineCalendarEvents',
targetRecord,
TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
);
const { timelineCalendarEvents, totalNumberOfCalendarEvents } =
- data?.[queryName] ?? {};
+ data?.getTimelineCalendarEventsFromObjectRecord ?? {};
const {
calendarEventsByDayTime,
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
index 2128194bf9..1ea46989f5 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
@@ -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 = {
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 = {
}
return HttpResponse.json({
data: {
- getTimelineCalendarEventsFromCompanyId: {
+ getTimelineCalendarEventsFromObjectRecord: {
__typename: 'TimelineCalendarEventsWithTotal',
totalNumberOfCalendarEvents: 3,
timelineCalendarEvents: mockedTimelineCalendarEvents,
diff --git a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromObjectRecord.ts
similarity index 57%
rename from packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts
rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromObjectRecord.ts
index c43d197e43..9e912f756e 100644
--- a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromObjectRecord.ts
@@ -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
) {
diff --git a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromOpportunityId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromOpportunityId.ts
deleted file mode 100644
index 84b0f69852..0000000000
--- a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromOpportunityId.ts
+++ /dev/null
@@ -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}
-`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts
deleted file mode 100644
index 3285fb475d..0000000000
--- a/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts
+++ /dev/null
@@ -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}
-`;
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
index 46e0d6e9be..e41406b141 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailsCard.tsx
@@ -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(
- 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
diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts
deleted file mode 100644
index 7c09d54871..0000000000
--- a/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts
+++ /dev/null
@@ -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();
- });
-});
diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts
deleted file mode 100644
index 1c0b1222d6..0000000000
--- a/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts
+++ /dev/null
@@ -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();
- });
-});
diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts
deleted file mode 100644
index e999e676b9..0000000000
--- a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts
+++ /dev/null
@@ -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}
-`;
diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord.ts
similarity index 57%
rename from packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId.ts
rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord.ts
index a085623987..569b2b52ee 100644
--- a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId.ts
+++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromObjectRecord.ts
@@ -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
) {
diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts
deleted file mode 100644
index 7f1877f112..0000000000
--- a/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts
+++ /dev/null
@@ -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}
-`;
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/useSendEmail.ts b/packages/twenty-front/src/modules/activities/emails/hooks/useSendEmail.ts
index 7157cc66ff..e7d8f66435 100644
--- a/packages/twenty-front/src/modules/activities/emails/hooks/useSendEmail.ts
+++ b/packages/twenty-front/src/modules/activities/emails/hooks/useSendEmail.ts
@@ -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',
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCustomResolver.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCustomResolver.test.tsx
new file mode 100644
index 0000000000..455152b886
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCustomResolver.test.tsx
@@ -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,
+ },
+ }),
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts b/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts
index 67d4356b27..f5dd006f70 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts
@@ -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,
};
diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts
index 6017807226..4637a6a0de 100644
--- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.module.ts
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
+import { RelatedPersonIdsModule } from 'src/engine/core-modules/related-person-ids/related-person-ids.module';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
@@ -11,6 +12,7 @@ import { UserModule } from 'src/engine/core-modules/user/user.module';
@Module({
imports: [
UserModule,
+ RelatedPersonIdsModule,
TypeOrmModule.forFeature([
CalendarChannelEntity,
ConnectedAccountEntity,
diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts
index b572b1f331..5cefc7c909 100644
--- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts
@@ -2,6 +2,7 @@ import { UseGuards } from '@nestjs/common';
import { Args, ArgsType, Field, Int, Query } from '@nestjs/graphql';
import { Max } from 'class-validator';
+import { CoreObjectNameSingular } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
@@ -14,6 +15,22 @@ import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
+@ArgsType()
+class GetTimelineCalendarEventsFromObjectRecordArgs {
+ @Field(() => String)
+ objectNameSingular: string;
+
+ @Field(() => UUIDScalarType)
+ recordId: string;
+
+ @Field(() => Int)
+ page: number;
+
+ @Field(() => Int)
+ @Max(TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE)
+ pageSize: number;
+}
+
@ArgsType()
class GetTimelineCalendarEventsFromPersonIdArgs {
@Field(() => UUIDScalarType)
@@ -61,44 +78,68 @@ export class TimelineCalendarEventResolver {
) {}
@Query(() => TimelineCalendarEventsWithTotalDTO)
+ async getTimelineCalendarEventsFromObjectRecord(
+ @Args()
+ {
+ objectNameSingular,
+ recordId,
+ page,
+ pageSize,
+ }: GetTimelineCalendarEventsFromObjectRecordArgs,
+ @AuthWorkspaceMemberId() workspaceMemberId: string,
+ @AuthWorkspace() workspace: WorkspaceEntity,
+ ) {
+ return this.timelineCalendarEventService.getCalendarEventsFromObjectRecord({
+ currentWorkspaceMemberId: workspaceMemberId,
+ objectNameSingular,
+ recordId,
+ workspaceId: workspace.id,
+ page,
+ pageSize,
+ });
+ }
+
+ @Query(() => TimelineCalendarEventsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineCalendarEventsFromObjectRecord instead',
+ })
async getTimelineCalendarEventsFromPersonId(
@Args()
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
- const timelineCalendarEvents =
- await this.timelineCalendarEventService.getCalendarEventsFromPersonIds({
- currentWorkspaceMemberId: workspaceMemberId,
- personIds: [personId],
- workspaceId: workspace.id,
- page,
- pageSize,
- });
-
- return timelineCalendarEvents;
+ return this.timelineCalendarEventService.getCalendarEventsFromObjectRecord({
+ currentWorkspaceMemberId: workspaceMemberId,
+ objectNameSingular: CoreObjectNameSingular.Person,
+ recordId: personId,
+ workspaceId: workspace.id,
+ page,
+ pageSize,
+ });
}
- @Query(() => TimelineCalendarEventsWithTotalDTO)
+ @Query(() => TimelineCalendarEventsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineCalendarEventsFromObjectRecord instead',
+ })
async getTimelineCalendarEventsFromCompanyId(
@Args()
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
- const timelineCalendarEvents =
- await this.timelineCalendarEventService.getCalendarEventsFromCompanyId({
- currentWorkspaceMemberId: workspaceMemberId,
- companyId,
- workspaceId: workspace.id,
- page,
- pageSize,
- });
-
- return timelineCalendarEvents;
+ return this.timelineCalendarEventService.getCalendarEventsFromObjectRecord({
+ currentWorkspaceMemberId: workspaceMemberId,
+ objectNameSingular: CoreObjectNameSingular.Company,
+ recordId: companyId,
+ workspaceId: workspace.id,
+ page,
+ pageSize,
+ });
}
- @Query(() => TimelineCalendarEventsWithTotalDTO)
+ @Query(() => TimelineCalendarEventsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineCalendarEventsFromObjectRecord instead',
+ })
async getTimelineCalendarEventsFromOpportunityId(
@Args()
{
@@ -109,17 +150,13 @@ export class TimelineCalendarEventResolver {
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
- const timelineCalendarEvents =
- await this.timelineCalendarEventService.getCalendarEventsFromOpportunityId(
- {
- currentWorkspaceMemberId: workspaceMemberId,
- opportunityId,
- workspaceId: workspace.id,
- page,
- pageSize,
- },
- );
-
- return timelineCalendarEvents;
+ return this.timelineCalendarEventService.getCalendarEventsFromObjectRecord({
+ currentWorkspaceMemberId: workspaceMemberId,
+ objectNameSingular: CoreObjectNameSingular.Opportunity,
+ recordId: opportunityId,
+ workspaceId: workspace.id,
+ page,
+ pageSize,
+ });
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts
index 6345d46e79..b6a50caea6 100644
--- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts
@@ -5,6 +5,7 @@ import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
+import { RelatedPersonIdsService } from 'src/engine/core-modules/related-person-ids/services/related-person-ids.service';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -96,6 +97,10 @@ describe('TimelineCalendarEventService', () => {
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
+ {
+ provide: RelatedPersonIdsService,
+ useValue: { getRelatedPersonIds: jest.fn().mockResolvedValue([]) },
+ },
],
}).compile();
diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts
index c384aaefe5..c794a638e6 100644
--- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts
@@ -8,14 +8,13 @@ import { Any, In, type Repository } from 'typeorm';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
+import { RelatedPersonIdsService } from 'src/engine/core-modules/related-person-ids/services/related-person-ids.service';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
-import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
-import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@@ -28,6 +27,7 @@ export class TimelineCalendarEventService {
private readonly connectedAccountRepository: Repository,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository,
+ private readonly relatedPersonIdsService: RelatedPersonIdsService,
) {}
async getCalendarEventsFromPersonIds({
@@ -259,113 +259,40 @@ export class TimelineCalendarEventService {
);
}
- async getCalendarEventsFromCompanyId({
+ async getCalendarEventsFromObjectRecord({
currentWorkspaceMemberId,
- companyId,
+ objectNameSingular,
+ recordId,
workspaceId,
page = 1,
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
}: {
currentWorkspaceMemberId: string;
- companyId: string;
+ objectNameSingular: string;
+ recordId: string;
workspaceId: string;
page: number;
pageSize: number;
}): Promise {
- const authContext = buildSystemAuthContext(workspaceId);
+ const personIds = await this.relatedPersonIdsService.getRelatedPersonIds({
+ workspaceId,
+ objectNameSingular,
+ recordId,
+ });
- return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
- async () => {
- const personRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'person',
- { shouldBypassPermissionChecks: true },
- );
+ if (personIds.length === 0) {
+ return {
+ totalNumberOfCalendarEvents: 0,
+ timelineCalendarEvents: [],
+ };
+ }
- const personIds = await personRepository.find({
- where: {
- companyId,
- },
- select: {
- id: true,
- },
- });
-
- if (personIds.length <= 0) {
- return {
- totalNumberOfCalendarEvents: 0,
- timelineCalendarEvents: [],
- };
- }
-
- const formattedPersonIds = personIds.map(({ id }) => id);
-
- const calendarEvents = await this.getCalendarEventsFromPersonIds({
- currentWorkspaceMemberId,
- personIds: formattedPersonIds,
- workspaceId,
- page,
- pageSize,
- });
-
- return calendarEvents;
- },
- authContext,
- );
- }
-
- async getCalendarEventsFromOpportunityId({
- currentWorkspaceMemberId,
- opportunityId,
- workspaceId,
- page = 1,
- pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
- }: {
- currentWorkspaceMemberId: string;
- opportunityId: string;
- workspaceId: string;
- page: number;
- pageSize: number;
- }): Promise {
- const authContext = buildSystemAuthContext(workspaceId);
-
- return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
- async () => {
- const opportunityRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'opportunity',
- { shouldBypassPermissionChecks: true },
- );
-
- const opportunity = await opportunityRepository.findOne({
- where: {
- id: opportunityId,
- },
- select: {
- companyId: true,
- },
- });
-
- if (!opportunity?.companyId) {
- return {
- totalNumberOfCalendarEvents: 0,
- timelineCalendarEvents: [],
- };
- }
-
- const calendarEvents = await this.getCalendarEventsFromCompanyId({
- currentWorkspaceMemberId,
- companyId: opportunity.companyId,
- workspaceId,
- page,
- pageSize,
- });
-
- return calendarEvents;
- },
- authContext,
- );
+ return this.getCalendarEventsFromPersonIds({
+ currentWorkspaceMemberId,
+ personIds,
+ workspaceId,
+ page,
+ pageSize,
+ });
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts
index 9967ae8e2c..c6c3212088 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts
@@ -4,16 +4,13 @@ import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/mess
import { type TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util';
-import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
-import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
-import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
-import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
+import { RelatedPersonIdsService } from 'src/engine/core-modules/related-person-ids/services/related-person-ids.service';
@Injectable()
export class GetMessagesService {
constructor(
- private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly timelineMessagingService: TimelineMessagingService,
+ private readonly relatedPersonIdsService: RelatedPersonIdsService,
) {}
async getMessagesFromPersonIds(
@@ -67,100 +64,33 @@ export class GetMessagesService {
};
}
- async getMessagesFromCompanyId(
+ async getMessagesFromObjectRecord(
workspaceMemberId: string,
- companyId: string,
+ objectNameSingular: string,
+ recordId: string,
workspaceId: string,
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise {
- const authContext = buildSystemAuthContext(workspaceId);
+ const personIds = await this.relatedPersonIdsService.getRelatedPersonIds({
+ workspaceId,
+ objectNameSingular,
+ recordId,
+ });
- return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
- async () => {
- const personRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'person',
- { shouldBypassPermissionChecks: true },
- );
- const personIds = (
- await personRepository.find({
- where: {
- companyId,
- },
- select: {
- id: true,
- },
- })
- ).map((person) => person.id);
+ if (personIds.length === 0) {
+ return {
+ totalNumberOfThreads: 0,
+ timelineThreads: [],
+ };
+ }
- if (personIds.length === 0) {
- return {
- totalNumberOfThreads: 0,
- timelineThreads: [],
- };
- }
-
- const messageThreads = await this.getMessagesFromPersonIds(
- workspaceMemberId,
- personIds,
- workspaceId,
- page,
- pageSize,
- );
-
- return messageThreads;
- },
- authContext,
- );
- }
-
- async getMessagesFromOpportunityId(
- workspaceMemberId: string,
- opportunityId: string,
- workspaceId: string,
- page = 1,
- pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
- ): Promise {
- const authContext = buildSystemAuthContext(workspaceId);
-
- return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
- async () => {
- const opportunityRepository =
- await this.globalWorkspaceOrmManager.getRepository(
- workspaceId,
- 'opportunity',
- { shouldBypassPermissionChecks: true },
- );
-
- const opportunity = await opportunityRepository.findOne({
- where: {
- id: opportunityId,
- },
- select: {
- companyId: true,
- },
- });
-
- if (!opportunity?.companyId) {
- return {
- totalNumberOfThreads: 0,
- timelineThreads: [],
- };
- }
-
- const messageThreads = await this.getMessagesFromCompanyId(
- workspaceMemberId,
- opportunity.companyId,
- workspaceId,
- page,
- pageSize,
- );
-
- return messageThreads;
- },
- authContext,
+ return this.getMessagesFromPersonIds(
+ workspaceMemberId,
+ personIds,
+ workspaceId,
+ page,
+ pageSize,
);
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
index 579aaefdcc..c2692a1082 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver';
+import { RelatedPersonIdsModule } from 'src/engine/core-modules/related-person-ids/related-person-ids.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@@ -20,6 +21,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
ConnectedAccountModule,
FeatureFlagModule,
PermissionsModule,
+ RelatedPersonIdsModule,
TypeOrmModule.forFeature([
MessageChannelEntity,
ConnectedAccountEntity,
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
index 1f38154119..91450fabd2 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
@@ -3,6 +3,7 @@ import { Args, ArgsType, Field, Int, Mutation, Query } from '@nestjs/graphql';
import { Max } from 'class-validator';
import { PermissionFlagType } from 'twenty-shared/constants';
+import { CoreObjectNameSingular } from 'twenty-shared/types';
import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -21,6 +22,22 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
+@ArgsType()
+class GetTimelineThreadsFromObjectRecordArgs {
+ @Field(() => String)
+ objectNameSingular: string;
+
+ @Field(() => UUIDScalarType)
+ recordId: string;
+
+ @Field(() => Int)
+ page: number;
+
+ @Field(() => Int)
+ @Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
+ pageSize: number;
+}
+
@ArgsType()
class GetTimelineThreadsFromPersonIdArgs {
@Field(() => UUIDScalarType)
@@ -70,6 +87,39 @@ export class TimelineMessagingResolver {
) {}
@Query(() => TimelineThreadsWithTotalDTO)
+ async getTimelineThreadsFromObjectRecord(
+ @AuthUser() user: AuthContextUser,
+ @AuthWorkspace() workspace: WorkspaceEntity,
+ @Args()
+ {
+ objectNameSingular,
+ recordId,
+ page,
+ pageSize,
+ }: GetTimelineThreadsFromObjectRecordArgs,
+ ) {
+ const workspaceMember = await this.userService.loadWorkspaceMember(
+ user,
+ workspace,
+ );
+
+ if (!workspaceMember) {
+ return;
+ }
+
+ return this.getMessagesFromPersonIdsService.getMessagesFromObjectRecord(
+ workspaceMember.id,
+ objectNameSingular,
+ recordId,
+ workspace.id,
+ page,
+ pageSize,
+ );
+ }
+
+ @Query(() => TimelineThreadsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineThreadsFromObjectRecord instead',
+ })
async getTimelineThreadsFromPersonId(
@AuthUser() user: AuthContextUser,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -84,19 +134,19 @@ export class TimelineMessagingResolver {
return;
}
- const timelineThreads =
- await this.getMessagesFromPersonIdsService.getMessagesFromPersonIds(
- workspaceMember.id,
- [personId],
- workspace.id,
- page,
- pageSize,
- );
-
- return timelineThreads;
+ return this.getMessagesFromPersonIdsService.getMessagesFromObjectRecord(
+ workspaceMember.id,
+ CoreObjectNameSingular.Person,
+ personId,
+ workspace.id,
+ page,
+ pageSize,
+ );
}
- @Query(() => TimelineThreadsWithTotalDTO)
+ @Query(() => TimelineThreadsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineThreadsFromObjectRecord instead',
+ })
async getTimelineThreadsFromCompanyId(
@AuthUser() user: AuthContextUser,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -111,19 +161,19 @@ export class TimelineMessagingResolver {
return;
}
- const timelineThreads =
- await this.getMessagesFromPersonIdsService.getMessagesFromCompanyId(
- workspaceMember.id,
- companyId,
- workspace.id,
- page,
- pageSize,
- );
-
- return timelineThreads;
+ return this.getMessagesFromPersonIdsService.getMessagesFromObjectRecord(
+ workspaceMember.id,
+ CoreObjectNameSingular.Company,
+ companyId,
+ workspace.id,
+ page,
+ pageSize,
+ );
}
- @Query(() => TimelineThreadsWithTotalDTO)
+ @Query(() => TimelineThreadsWithTotalDTO, {
+ deprecationReason: 'Use getTimelineThreadsFromObjectRecord instead',
+ })
async getTimelineThreadsFromOpportunityId(
@AuthUser() user: AuthContextUser,
@AuthWorkspace() workspace: WorkspaceEntity,
@@ -139,16 +189,14 @@ export class TimelineMessagingResolver {
return;
}
- const timelineThreads =
- await this.getMessagesFromPersonIdsService.getMessagesFromOpportunityId(
- workspaceMember.id,
- opportunityId,
- workspace.id,
- page,
- pageSize,
- );
-
- return timelineThreads;
+ return this.getMessagesFromPersonIdsService.getMessagesFromObjectRecord(
+ workspaceMember.id,
+ CoreObjectNameSingular.Opportunity,
+ opportunityId,
+ workspace.id,
+ page,
+ pageSize,
+ );
}
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
diff --git a/packages/twenty-server/src/engine/core-modules/related-person-ids/related-person-ids.module.ts b/packages/twenty-server/src/engine/core-modules/related-person-ids/related-person-ids.module.ts
new file mode 100644
index 0000000000..3277014709
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/related-person-ids/related-person-ids.module.ts
@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common';
+
+import { RelatedPersonIdsService } from 'src/engine/core-modules/related-person-ids/services/related-person-ids.service';
+import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
+
+@Module({
+ imports: [WorkspaceCacheModule],
+ providers: [RelatedPersonIdsService],
+ exports: [RelatedPersonIdsService],
+})
+export class RelatedPersonIdsModule {}
diff --git a/packages/twenty-server/src/engine/core-modules/related-person-ids/services/related-person-ids.service.ts b/packages/twenty-server/src/engine/core-modules/related-person-ids/services/related-person-ids.service.ts
new file mode 100644
index 0000000000..b71da64ee4
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/related-person-ids/services/related-person-ids.service.ts
@@ -0,0 +1,130 @@
+import { Injectable } from '@nestjs/common';
+
+import { isDefined } from 'twenty-shared/utils';
+import { In, type FindOptionsSelect, type FindOptionsWhere } from 'typeorm';
+
+import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
+import {
+ findRelationPathsToPerson,
+ type RelationPathToPerson,
+} from 'src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util';
+import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
+import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
+import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
+
+const PERSON_OBJECT_NAME_SINGULAR = 'person';
+
+type RelationWalkRecord = { id: string } & Record;
+
+@Injectable()
+export class RelatedPersonIdsService {
+ constructor(
+ private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
+ private readonly workspaceCacheService: WorkspaceCacheService,
+ ) {}
+
+ async getRelatedPersonIds({
+ workspaceId,
+ objectNameSingular,
+ recordId,
+ }: {
+ workspaceId: string;
+ objectNameSingular: string;
+ recordId: string;
+ }): Promise {
+ if (objectNameSingular === PERSON_OBJECT_NAME_SINGULAR) {
+ return [recordId];
+ }
+
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatObjectMetadataMaps',
+ 'flatFieldMetadataMaps',
+ ]);
+
+ const relationPaths = findRelationPathsToPerson({
+ rootObjectNameSingular: objectNameSingular,
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ });
+
+ if (relationPaths.length === 0) {
+ return [];
+ }
+
+ return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
+ async () => {
+ const personIds = new Set();
+
+ for (const relationPath of relationPaths) {
+ const personIdsForPath = await this.walkRelationPath({
+ workspaceId,
+ recordId,
+ relationPath,
+ });
+
+ personIdsForPath.forEach((personId) => personIds.add(personId));
+ }
+
+ return [...personIds];
+ },
+ buildSystemAuthContext(workspaceId),
+ );
+ }
+
+ private async walkRelationPath({
+ workspaceId,
+ recordId,
+ relationPath,
+ }: {
+ workspaceId: string;
+ recordId: string;
+ relationPath: RelationPathToPerson;
+ }): Promise {
+ let currentIds = [recordId];
+
+ for (const hop of relationPath) {
+ if (currentIds.length === 0) {
+ return [];
+ }
+
+ const repository =
+ await this.globalWorkspaceOrmManager.getRepository(
+ workspaceId,
+ hop.queryObjectNameSingular,
+ { shouldBypassPermissionChecks: true },
+ );
+
+ if (hop.direction === RelationType.MANY_TO_ONE) {
+ const records = await repository.find({
+ where: { id: In(currentIds) } as FindOptionsWhere,
+ select: {
+ [hop.joinColumnName]: true,
+ } as FindOptionsSelect,
+ });
+
+ currentIds = [
+ ...new Set(
+ records
+ .map((record) => record[hop.joinColumnName])
+ .filter(
+ (value): value is string =>
+ typeof value === 'string' && isDefined(value),
+ ),
+ ),
+ ];
+ } else {
+ const records = await repository.find({
+ where: {
+ [hop.joinColumnName]: In(currentIds),
+ } as FindOptionsWhere,
+ select: { id: true },
+ });
+
+ currentIds = [...new Set(records.map((record) => record.id))];
+ }
+ }
+
+ return currentIds;
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/__tests__/find-relation-paths-to-person.util.spec.ts b/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/__tests__/find-relation-paths-to-person.util.spec.ts
new file mode 100644
index 0000000000..04717ee5b6
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/__tests__/find-relation-paths-to-person.util.spec.ts
@@ -0,0 +1,294 @@
+import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
+import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
+import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
+import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
+import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
+import { findRelationPathsToPerson } from 'src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util';
+
+jest.mock(
+ 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util',
+);
+
+const resolveRelationMock = jest.mocked(resolveRelationFromFlatFieldMetadata);
+
+type RelationSpec = {
+ fieldName: string;
+ relationType: RelationType;
+ targetObjectNameSingular: string;
+ inverseFieldName: string;
+};
+
+const buildGraphFixtures = (graph: Record) => {
+ const fieldId = (objectNameSingular: string, fieldName: string) =>
+ `${objectNameSingular}.${fieldName}`;
+
+ const flatObjectMetadataMaps = {
+ byUniversalIdentifier: {},
+ universalIdentifierById: {},
+ } as unknown as FlatEntityMaps;
+
+ const flatFieldMetadataMaps = {
+ byUniversalIdentifier: {},
+ universalIdentifierById: {},
+ } as unknown as FlatEntityMaps;
+
+ const relationSpecByFieldId = new Map<
+ string,
+ { sourceObjectNameSingular: string; spec: RelationSpec }
+ >();
+
+ for (const [objectNameSingular, relationSpecs] of Object.entries(graph)) {
+ flatObjectMetadataMaps.byUniversalIdentifier[objectNameSingular] = {
+ id: objectNameSingular,
+ nameSingular: objectNameSingular,
+ fieldIds: relationSpecs.map((spec) =>
+ fieldId(objectNameSingular, spec.fieldName),
+ ),
+ } as unknown as FlatObjectMetadata;
+ flatObjectMetadataMaps.universalIdentifierById[objectNameSingular] =
+ objectNameSingular;
+
+ for (const spec of relationSpecs) {
+ const id = fieldId(objectNameSingular, spec.fieldName);
+
+ flatFieldMetadataMaps.byUniversalIdentifier[id] = {
+ id,
+ type: 'RELATION',
+ } as unknown as FlatFieldMetadata;
+ flatFieldMetadataMaps.universalIdentifierById[id] = id;
+
+ relationSpecByFieldId.set(id, {
+ sourceObjectNameSingular: objectNameSingular,
+ spec,
+ });
+ }
+ }
+
+ resolveRelationMock.mockImplementation(({ sourceFlatFieldMetadata }) => {
+ const resolved = relationSpecByFieldId.get(sourceFlatFieldMetadata.id);
+
+ if (resolved === undefined) {
+ return null;
+ }
+
+ const { sourceObjectNameSingular, spec } = resolved;
+
+ return {
+ type: spec.relationType,
+ sourceObjectMetadata: {
+ id: sourceObjectNameSingular,
+ nameSingular: sourceObjectNameSingular,
+ },
+ targetObjectMetadata: {
+ id: spec.targetObjectNameSingular,
+ nameSingular: spec.targetObjectNameSingular,
+ },
+ sourceFieldMetadata: { name: spec.fieldName },
+ targetFieldMetadata: { name: spec.inverseFieldName },
+ } as ReturnType;
+ });
+
+ return { flatObjectMetadataMaps, flatFieldMetadataMaps };
+};
+
+afterEach(() => {
+ resolveRelationMock.mockReset();
+});
+
+describe('findRelationPathsToPerson', () => {
+ it('returns the empty path for the person object itself', () => {
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ buildGraphFixtures({ person: [] });
+
+ expect(
+ findRelationPathsToPerson({
+ rootObjectNameSingular: 'person',
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ }),
+ ).toEqual([[]]);
+ });
+
+ it('resolves a direct relation to person, querying person by its foreign key', () => {
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ buildGraphFixtures({
+ company: [
+ {
+ fieldName: 'people',
+ relationType: RelationType.ONE_TO_MANY,
+ targetObjectNameSingular: 'person',
+ inverseFieldName: 'company',
+ },
+ ],
+ person: [
+ {
+ fieldName: 'company',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'company',
+ inverseFieldName: 'people',
+ },
+ ],
+ });
+
+ expect(
+ findRelationPathsToPerson({
+ rootObjectNameSingular: 'company',
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ }),
+ ).toEqual([
+ [
+ {
+ direction: RelationType.ONE_TO_MANY,
+ queryObjectNameSingular: 'person',
+ joinColumnName: 'companyId',
+ },
+ ],
+ ]);
+ });
+
+ it('resolves person through a two-hop join object', () => {
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ buildGraphFixtures({
+ peopleList: [
+ {
+ fieldName: 'peopleListMemberships',
+ relationType: RelationType.ONE_TO_MANY,
+ targetObjectNameSingular: 'peopleListMembership',
+ inverseFieldName: 'peopleList',
+ },
+ ],
+ peopleListMembership: [
+ {
+ fieldName: 'peopleList',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'peopleList',
+ inverseFieldName: 'peopleListMemberships',
+ },
+ {
+ fieldName: 'person',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'person',
+ inverseFieldName: 'peopleListMemberships',
+ },
+ ],
+ person: [
+ {
+ fieldName: 'peopleListMemberships',
+ relationType: RelationType.ONE_TO_MANY,
+ targetObjectNameSingular: 'peopleListMembership',
+ inverseFieldName: 'person',
+ },
+ ],
+ });
+
+ expect(
+ findRelationPathsToPerson({
+ rootObjectNameSingular: 'peopleList',
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ }),
+ ).toEqual([
+ [
+ {
+ direction: RelationType.ONE_TO_MANY,
+ queryObjectNameSingular: 'peopleListMembership',
+ joinColumnName: 'peopleListId',
+ },
+ {
+ direction: RelationType.MANY_TO_ONE,
+ queryObjectNameSingular: 'peopleListMembership',
+ joinColumnName: 'personId',
+ },
+ ],
+ ]);
+ });
+
+ it('collects both a direct and a longer relation chain to person (opportunity)', () => {
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ buildGraphFixtures({
+ opportunity: [
+ {
+ fieldName: 'pointOfContact',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'person',
+ inverseFieldName: 'pointOfContactForOpportunities',
+ },
+ {
+ fieldName: 'company',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'company',
+ inverseFieldName: 'opportunities',
+ },
+ ],
+ company: [
+ {
+ fieldName: 'people',
+ relationType: RelationType.ONE_TO_MANY,
+ targetObjectNameSingular: 'person',
+ inverseFieldName: 'company',
+ },
+ ],
+ person: [],
+ });
+
+ expect(
+ findRelationPathsToPerson({
+ rootObjectNameSingular: 'opportunity',
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ }),
+ ).toEqual([
+ [
+ {
+ direction: RelationType.MANY_TO_ONE,
+ queryObjectNameSingular: 'opportunity',
+ joinColumnName: 'pointOfContactId',
+ },
+ ],
+ [
+ {
+ direction: RelationType.MANY_TO_ONE,
+ queryObjectNameSingular: 'opportunity',
+ joinColumnName: 'companyId',
+ },
+ {
+ direction: RelationType.ONE_TO_MANY,
+ queryObjectNameSingular: 'person',
+ joinColumnName: 'companyId',
+ },
+ ],
+ ]);
+ });
+
+ it('returns no path when person is unreachable, terminating on relation cycles', () => {
+ const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
+ buildGraphFixtures({
+ rocket: [
+ {
+ fieldName: 'cells',
+ relationType: RelationType.ONE_TO_MANY,
+ targetObjectNameSingular: 'rocketCell',
+ inverseFieldName: 'rocket',
+ },
+ ],
+ rocketCell: [
+ {
+ fieldName: 'rocket',
+ relationType: RelationType.MANY_TO_ONE,
+ targetObjectNameSingular: 'rocket',
+ inverseFieldName: 'cells',
+ },
+ ],
+ person: [],
+ });
+
+ expect(
+ findRelationPathsToPerson({
+ rootObjectNameSingular: 'rocket',
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ }),
+ ).toEqual([]);
+ });
+});
diff --git a/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util.ts b/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util.ts
new file mode 100644
index 0000000000..0cc45716c2
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/related-person-ids/utils/find-relation-paths-to-person.util.ts
@@ -0,0 +1,125 @@
+import { FieldMetadataType } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
+import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
+import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
+import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
+import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
+import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
+import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
+import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
+import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
+import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
+
+const PERSON_OBJECT_NAME_SINGULAR = 'person';
+const DEFAULT_MAX_RELATION_DEPTH_TO_PERSON = 3;
+
+export type RelationHopToPerson = {
+ direction: RelationType;
+ queryObjectNameSingular: string;
+ joinColumnName: string;
+};
+
+export type RelationPathToPerson = RelationHopToPerson[];
+
+export const findRelationPathsToPerson = ({
+ rootObjectNameSingular,
+ flatObjectMetadataMaps,
+ flatFieldMetadataMaps,
+ maxDepth = DEFAULT_MAX_RELATION_DEPTH_TO_PERSON,
+}: {
+ rootObjectNameSingular: string;
+ flatObjectMetadataMaps: FlatEntityMaps;
+ flatFieldMetadataMaps: FlatEntityMaps;
+ maxDepth?: number;
+}): RelationPathToPerson[] => {
+ if (rootObjectNameSingular === PERSON_OBJECT_NAME_SINGULAR) {
+ return [[]];
+ }
+
+ const { idByNameSingular } = buildObjectIdByNameMaps(flatObjectMetadataMaps);
+ const rootObjectId = idByNameSingular[rootObjectNameSingular];
+
+ if (!isDefined(rootObjectId)) {
+ return [];
+ }
+
+ let frontier = [{ objectId: rootObjectId, path: [] as RelationPathToPerson }];
+ const visitedObjectIds = new Set([rootObjectId]);
+ const pathsToPerson: RelationPathToPerson[] = [];
+
+ for (let depth = 0; depth < maxDepth; depth++) {
+ const nextFrontier: typeof frontier = [];
+ const objectIdsReachedThisDepth = new Set();
+
+ for (const { objectId, path } of frontier) {
+ const sourceObject = findFlatEntityByIdInFlatEntityMaps({
+ flatEntityId: objectId,
+ flatEntityMaps: flatObjectMetadataMaps,
+ });
+
+ if (!isDefined(sourceObject)) {
+ continue;
+ }
+
+ for (const field of getFlatFieldsFromFlatObjectMetadata(
+ sourceObject,
+ flatFieldMetadataMaps,
+ )) {
+ if (!isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) {
+ continue;
+ }
+
+ const relation = resolveRelationFromFlatFieldMetadata({
+ sourceFlatFieldMetadata: field,
+ flatFieldMetadataMaps,
+ flatObjectMetadataMaps,
+ });
+
+ if (!isDefined(relation)) {
+ continue;
+ }
+
+ const joinColumnOwner =
+ relation.type === RelationType.MANY_TO_ONE
+ ? {
+ object: relation.sourceObjectMetadata,
+ field: relation.sourceFieldMetadata,
+ }
+ : {
+ object: relation.targetObjectMetadata,
+ field: relation.targetFieldMetadata,
+ };
+
+ const nextPath: RelationPathToPerson = [
+ ...path,
+ {
+ direction: relation.type,
+ queryObjectNameSingular: joinColumnOwner.object.nameSingular,
+ joinColumnName: computeMorphOrRelationFieldJoinColumnName({
+ name: joinColumnOwner.field.name,
+ }),
+ },
+ ];
+
+ const targetObjectId = relation.targetObjectMetadata.id;
+
+ if (
+ relation.targetObjectMetadata.nameSingular ===
+ PERSON_OBJECT_NAME_SINGULAR
+ ) {
+ pathsToPerson.push(nextPath);
+ } else if (!visitedObjectIds.has(targetObjectId)) {
+ objectIdsReachedThisDepth.add(targetObjectId);
+ nextFrontier.push({ objectId: targetObjectId, path: nextPath });
+ }
+ }
+ }
+
+ objectIdsReachedThisDepth.forEach((id) => visitedObjectIds.add(id));
+ frontier = nextFrontier;
+ }
+
+ return pathsToPerson;
+};
diff --git a/packages/twenty-server/test/integration/graphql/suites/timeline-from-object-record.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/timeline-from-object-record.integration-spec.ts
new file mode 100644
index 0000000000..0fd34c0293
--- /dev/null
+++ b/packages/twenty-server/test/integration/graphql/suites/timeline-from-object-record.integration-spec.ts
@@ -0,0 +1,396 @@
+import gql from 'graphql-tag';
+import { type DocumentNode } from 'graphql';
+import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
+import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
+import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
+import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
+import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
+import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
+
+const PAGE_SIZE = 50;
+const PEOPLE_DISCOVERY_LIMIT = 100;
+
+const GET_TIMELINE_THREADS = gql`
+ query GetTimelineThreadsFromObjectRecord(
+ $objectNameSingular: String!
+ $recordId: UUID!
+ $page: Int!
+ $pageSize: Int!
+ ) {
+ getTimelineThreadsFromObjectRecord(
+ objectNameSingular: $objectNameSingular
+ recordId: $recordId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ totalNumberOfThreads
+ timelineThreads {
+ id
+ }
+ }
+ }
+`;
+
+const GET_TIMELINE_CALENDAR_EVENTS = gql`
+ query GetTimelineCalendarEventsFromObjectRecord(
+ $objectNameSingular: String!
+ $recordId: UUID!
+ $page: Int!
+ $pageSize: Int!
+ ) {
+ getTimelineCalendarEventsFromObjectRecord(
+ objectNameSingular: $objectNameSingular
+ recordId: $recordId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ totalNumberOfCalendarEvents
+ timelineCalendarEvents {
+ id
+ }
+ }
+ }
+`;
+
+const THREADS_SELECTION = 'totalNumberOfThreads timelineThreads { id }';
+const CALENDAR_SELECTION =
+ 'totalNumberOfCalendarEvents timelineCalendarEvents { id }';
+
+const buildLegacyQuery = (
+ queryName: string,
+ idArgName: string,
+ selection: string,
+): DocumentNode =>
+ gql`
+ query ${queryName}($id: UUID!, $page: Int!, $pageSize: Int!) {
+ ${queryName}(${idArgName}: $id, page: $page, pageSize: $pageSize) {
+ ${selection}
+ }
+ }
+ `;
+
+const requestTimeline = (
+ query: DocumentNode,
+ objectNameSingular: string,
+ recordId: string,
+) =>
+ makeGraphqlAPIRequest({
+ query,
+ variables: { objectNameSingular, recordId, page: 1, pageSize: PAGE_SIZE },
+ });
+
+const getPeopleWithCompany = async (): Promise<
+ { id: string; companyId: string }[]
+> => {
+ const response = await makeGraphqlAPIRequest(
+ findManyOperationFactory({
+ objectMetadataSingularName: 'person',
+ objectMetadataPluralName: 'people',
+ gqlFields: 'id company { id }',
+ first: PEOPLE_DISCOVERY_LIMIT,
+ }),
+ );
+
+ expect(response.body.errors).toBeUndefined();
+
+ return (
+ response.body.data.people.edges
+ // @ts-expect-error legacy noImplicitAny
+ .map((edge) => edge.node)
+ // @ts-expect-error legacy noImplicitAny
+ .filter((person) => person.company?.id)
+ // @ts-expect-error legacy noImplicitAny
+ .map((person) => ({ id: person.id, companyId: person.company.id }))
+ );
+};
+
+const findPersonWithThreads = async (
+ people: { id: string; companyId: string }[],
+) => {
+ for (const person of people) {
+ const response = await requestTimeline(
+ GET_TIMELINE_THREADS,
+ 'person',
+ person.id,
+ );
+
+ if (
+ response.body.data.getTimelineThreadsFromObjectRecord
+ .totalNumberOfThreads > 0
+ ) {
+ return person;
+ }
+ }
+
+ return undefined;
+};
+
+const findPersonWithCalendarEvents = async (
+ people: { id: string; companyId: string }[],
+) => {
+ for (const person of people) {
+ const response = await requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ 'person',
+ person.id,
+ );
+
+ if (
+ response.body.data.getTimelineCalendarEventsFromObjectRecord
+ .totalNumberOfCalendarEvents > 0
+ ) {
+ return person;
+ }
+ }
+
+ return undefined;
+};
+
+describe('timeline from object record resolvers (integration)', () => {
+ let personWithThreads: { id: string; companyId: string };
+ let personWithEvents: { id: string; companyId: string };
+
+ beforeAll(async () => {
+ const people = await getPeopleWithCompany();
+
+ const threadsSource = await findPersonWithThreads(people);
+ const eventsSource = await findPersonWithCalendarEvents(people);
+
+ if (!threadsSource || !eventsSource) {
+ throw new Error(
+ 'Expected the seeded workspace to contain a person with message threads and calendar events',
+ );
+ }
+
+ personWithThreads = threadsSource;
+ personWithEvents = eventsSource;
+ });
+
+ it('should derive a company message timeline from its related people', async () => {
+ const [personResponse, companyResponse] = await Promise.all([
+ requestTimeline(GET_TIMELINE_THREADS, 'person', personWithThreads.id),
+ requestTimeline(
+ GET_TIMELINE_THREADS,
+ 'company',
+ personWithThreads.companyId,
+ ),
+ ]);
+
+ expect(personResponse.body.errors).toBeUndefined();
+ expect(companyResponse.body.errors).toBeUndefined();
+
+ const personThreadCount =
+ personResponse.body.data.getTimelineThreadsFromObjectRecord
+ .totalNumberOfThreads;
+ const companyThreadCount =
+ companyResponse.body.data.getTimelineThreadsFromObjectRecord
+ .totalNumberOfThreads;
+
+ expect(personThreadCount).toBeGreaterThan(0);
+ expect(companyThreadCount).toBeGreaterThanOrEqual(personThreadCount);
+ });
+
+ it('should derive a company calendar timeline from its related people', async () => {
+ const [personResponse, companyResponse] = await Promise.all([
+ requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ 'person',
+ personWithEvents.id,
+ ),
+ requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ 'company',
+ personWithEvents.companyId,
+ ),
+ ]);
+
+ expect(personResponse.body.errors).toBeUndefined();
+ expect(companyResponse.body.errors).toBeUndefined();
+
+ const personEventCount =
+ personResponse.body.data.getTimelineCalendarEventsFromObjectRecord
+ .totalNumberOfCalendarEvents;
+ const companyEventCount =
+ companyResponse.body.data.getTimelineCalendarEventsFromObjectRecord
+ .totalNumberOfCalendarEvents;
+
+ expect(personEventCount).toBeGreaterThan(0);
+ expect(companyEventCount).toBeGreaterThanOrEqual(personEventCount);
+ });
+
+ it('should serve getTimelineThreadsFromPersonId identically to the object-record resolver', async () => {
+ const [fromObjectRecord, fromLegacy] = await Promise.all([
+ requestTimeline(GET_TIMELINE_THREADS, 'person', personWithThreads.id),
+ makeGraphqlAPIRequest({
+ query: buildLegacyQuery(
+ 'getTimelineThreadsFromPersonId',
+ 'personId',
+ THREADS_SELECTION,
+ ),
+ variables: { id: personWithThreads.id, page: 1, pageSize: PAGE_SIZE },
+ }),
+ ]);
+
+ expect(fromLegacy.body.errors).toBeUndefined();
+ expect(fromLegacy.body.data.getTimelineThreadsFromPersonId).toEqual(
+ fromObjectRecord.body.data.getTimelineThreadsFromObjectRecord,
+ );
+ });
+
+ it('should serve getTimelineThreadsFromCompanyId identically to the object-record resolver', async () => {
+ const [fromObjectRecord, fromLegacy] = await Promise.all([
+ requestTimeline(
+ GET_TIMELINE_THREADS,
+ 'company',
+ personWithThreads.companyId,
+ ),
+ makeGraphqlAPIRequest({
+ query: buildLegacyQuery(
+ 'getTimelineThreadsFromCompanyId',
+ 'companyId',
+ THREADS_SELECTION,
+ ),
+ variables: {
+ id: personWithThreads.companyId,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ },
+ }),
+ ]);
+
+ expect(fromLegacy.body.errors).toBeUndefined();
+ expect(fromLegacy.body.data.getTimelineThreadsFromCompanyId).toEqual(
+ fromObjectRecord.body.data.getTimelineThreadsFromObjectRecord,
+ );
+ });
+
+ it('should serve getTimelineCalendarEventsFromPersonId identically to the object-record resolver', async () => {
+ const [fromObjectRecord, fromLegacy] = await Promise.all([
+ requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ 'person',
+ personWithEvents.id,
+ ),
+ makeGraphqlAPIRequest({
+ query: buildLegacyQuery(
+ 'getTimelineCalendarEventsFromPersonId',
+ 'personId',
+ CALENDAR_SELECTION,
+ ),
+ variables: { id: personWithEvents.id, page: 1, pageSize: PAGE_SIZE },
+ }),
+ ]);
+
+ expect(fromLegacy.body.errors).toBeUndefined();
+ expect(fromLegacy.body.data.getTimelineCalendarEventsFromPersonId).toEqual(
+ fromObjectRecord.body.data.getTimelineCalendarEventsFromObjectRecord,
+ );
+ });
+
+ it('should serve getTimelineCalendarEventsFromCompanyId identically to the object-record resolver', async () => {
+ const [fromObjectRecord, fromLegacy] = await Promise.all([
+ requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ 'company',
+ personWithEvents.companyId,
+ ),
+ makeGraphqlAPIRequest({
+ query: buildLegacyQuery(
+ 'getTimelineCalendarEventsFromCompanyId',
+ 'companyId',
+ CALENDAR_SELECTION,
+ ),
+ variables: {
+ id: personWithEvents.companyId,
+ page: 1,
+ pageSize: PAGE_SIZE,
+ },
+ }),
+ ]);
+
+ expect(fromLegacy.body.errors).toBeUndefined();
+ expect(fromLegacy.body.data.getTimelineCalendarEventsFromCompanyId).toEqual(
+ fromObjectRecord.body.data.getTimelineCalendarEventsFromObjectRecord,
+ );
+ });
+
+ describe('with a custom object that has no people on its timeline', () => {
+ const CUSTOM_OBJECT_NAME_SINGULAR = 'timelineProbe';
+ const CUSTOM_RECORD_ID = '20202020-cccc-4000-8000-0000000000a1';
+
+ let customObjectMetadataId: string;
+
+ beforeAll(async () => {
+ const { data } = await createOneObjectMetadata({
+ input: {
+ nameSingular: CUSTOM_OBJECT_NAME_SINGULAR,
+ namePlural: 'timelineProbes',
+ labelSingular: 'Timeline Probe',
+ labelPlural: 'Timeline Probes',
+ icon: 'IconRadar',
+ },
+ expectToFail: false,
+ });
+
+ customObjectMetadataId = data.createOneObject.id;
+
+ await makeGraphqlAPIRequest(
+ createOneOperationFactory({
+ objectMetadataSingularName: CUSTOM_OBJECT_NAME_SINGULAR,
+ gqlFields: 'id',
+ data: { id: CUSTOM_RECORD_ID, name: 'Probe' },
+ }),
+ );
+ });
+
+ afterAll(async () => {
+ await updateOneObjectMetadata({
+ input: {
+ idToUpdate: customObjectMetadataId,
+ updatePayload: { isActive: false },
+ },
+ expectToFail: false,
+ });
+
+ await deleteOneObjectMetadata({
+ input: { idToDelete: customObjectMetadataId },
+ expectToFail: false,
+ });
+ });
+
+ it('should return an empty message timeline instead of crashing', async () => {
+ const response = await requestTimeline(
+ GET_TIMELINE_THREADS,
+ CUSTOM_OBJECT_NAME_SINGULAR,
+ CUSTOM_RECORD_ID,
+ );
+
+ expect(response.body.errors).toBeUndefined();
+ expect(
+ response.body.data.getTimelineThreadsFromObjectRecord
+ .totalNumberOfThreads,
+ ).toBe(0);
+ expect(
+ response.body.data.getTimelineThreadsFromObjectRecord.timelineThreads,
+ ).toEqual([]);
+ });
+
+ it('should return an empty calendar timeline instead of crashing', async () => {
+ const response = await requestTimeline(
+ GET_TIMELINE_CALENDAR_EVENTS,
+ CUSTOM_OBJECT_NAME_SINGULAR,
+ CUSTOM_RECORD_ID,
+ );
+
+ expect(response.body.errors).toBeUndefined();
+ expect(
+ response.body.data.getTimelineCalendarEventsFromObjectRecord
+ .totalNumberOfCalendarEvents,
+ ).toBe(0);
+ expect(
+ response.body.data.getTimelineCalendarEventsFromObjectRecord
+ .timelineCalendarEvents,
+ ).toEqual([]);
+ });
+ });
+});