feat: email and calendar events for opportunities (#13613)

This commit is contained in:
neo773
2025-08-04 21:43:09 +00:00
committed by GitHub
parent 2a0c73d276
commit 19dbe28786
12 changed files with 284 additions and 6 deletions
@@ -2109,8 +2109,10 @@ export type Query = {
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
getSystemHealthStatus: SystemHealth;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
index: Index;
indexMetadatas: IndexConnection;
@@ -2269,6 +2271,13 @@ export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
};
export type QueryGetTimelineCalendarEventsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
@@ -2283,6 +2292,13 @@ export type QueryGetTimelineThreadsFromCompanyIdArgs = {
};
export type QueryGetTimelineThreadsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineThreadsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
@@ -2017,8 +2017,10 @@ export type Query = {
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
getSystemHealthStatus: SystemHealth;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
index: Index;
indexMetadatas: IndexConnection;
@@ -2151,6 +2153,13 @@ export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
};
export type QueryGetTimelineCalendarEventsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
@@ -2165,6 +2174,13 @@ export type QueryGetTimelineThreadsFromCompanyIdArgs = {
};
export type QueryGetTimelineThreadsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineThreadsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
@@ -5,6 +5,7 @@ import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMont
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 { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
@@ -53,10 +54,16 @@ export const Calendar = ({
getTimelineCalendarEventsFromPersonId,
'getTimelineCalendarEventsFromPersonId',
]
: [
getTimelineCalendarEventsFromCompanyId,
'getTimelineCalendarEventsFromCompanyId',
];
: targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Company
? [
getTimelineCalendarEventsFromCompanyId,
'getTimelineCalendarEventsFromCompanyId',
]
: [
getTimelineCalendarEventsFromOpportunityId,
'getTimelineCalendarEventsFromOpportunityId',
];
const { data, firstQueryLoading, isFetchingMore, fetchMoreRecords } =
useCustomResolver<TimelineCalendarEventsWithTotal>(
@@ -0,0 +1,20 @@
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}
`;
@@ -7,6 +7,7 @@ import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPr
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging';
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId';
import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId';
import { getTimelineThreadsFromOpportunityId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromOpportunityId';
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -48,7 +49,13 @@ export const EmailThreads = ({
const [query, queryName] =
targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person
? [getTimelineThreadsFromPersonId, 'getTimelineThreadsFromPersonId']
: [getTimelineThreadsFromCompanyId, 'getTimelineThreadsFromCompanyId'];
: targetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Company
? [getTimelineThreadsFromCompanyId, 'getTimelineThreadsFromCompanyId']
: [
getTimelineThreadsFromOpportunityId,
'getTimelineThreadsFromOpportunityId',
];
const { data, firstQueryLoading, isFetchingMore, fetchMoreRecords } =
useCustomResolver<TimelineThreadsWithTotal>(
@@ -0,0 +1,20 @@
import { gql } from '@apollo/client';
import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment';
export const getTimelineThreadsFromOpportunityId = gql`
query GetTimelineThreadsFromOpportunityId(
$opportunityId: UUID!
$page: Int!
$pageSize: Int!
) {
getTimelineThreadsFromOpportunityId(
opportunityId: $opportunityId
page: $page
pageSize: $pageSize
) {
...TimelineThreadsWithTotalFragment
}
}
${timelineThreadWithTotalFragment}
`;
@@ -49,7 +49,10 @@ export const useCustomResolver = <
...(activityTargetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Person
? { personId: activityTargetableObject.id }
: { companyId: activityTargetableObject.id }),
: activityTargetableObject.targetObjectNameSingular ===
CoreObjectNameSingular.Opportunity
? { opportunityId: activityTargetableObject.id }
: { companyId: activityTargetableObject.id }),
page: 1,
pageSize,
};
@@ -142,6 +142,38 @@ export const useRecordShowContainerTabs = (
},
},
},
[CoreObjectNameSingular.Opportunity]: {
tabs: {
emails: {
title: 'Emails',
position: 600,
Icon: IconMail,
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
calendar: {
title: 'Calendar',
position: 700,
Icon: IconCalendarEvent,
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
},
[CoreObjectNameSingular.Workflow]: {
hideSummaryAndFields: true,
tabs: {
@@ -36,6 +36,19 @@ class GetTimelineCalendarEventsFromCompanyIdArgs {
pageSize: number;
}
@ArgsType()
class GetTimelineCalendarEventsFromOpportunityIdArgs {
@Field(() => UUIDScalarType)
opportunityId: string;
@Field(() => Int)
page: number;
@Field(() => Int)
@Max(TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE)
pageSize: number;
}
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => TimelineCalendarEventsWithTotal)
export class TimelineCalendarEventResolver {
@@ -76,4 +89,27 @@ export class TimelineCalendarEventResolver {
return timelineCalendarEvents;
}
@Query(() => TimelineCalendarEventsWithTotal)
async getTimelineCalendarEventsFromOpportunityId(
@Args()
{
opportunityId,
page,
pageSize,
}: GetTimelineCalendarEventsFromOpportunityIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
) {
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromOpportunityId(
{
currentWorkspaceMemberId: workspaceMemberId,
opportunityId,
page,
pageSize,
},
);
return timelineCalendarEvents;
}
}
@@ -9,6 +9,7 @@ import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calenda
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Injectable()
@@ -199,4 +200,46 @@ export class TimelineCalendarEventService {
return calendarEvents;
}
async getCalendarEventsFromOpportunityId({
currentWorkspaceMemberId,
opportunityId,
page = 1,
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
}: {
currentWorkspaceMemberId: string;
opportunityId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotal> {
const opportunityRepository =
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
'opportunity',
);
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,
page,
pageSize,
});
return calendarEvents;
}
}
@@ -5,6 +5,7 @@ import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos
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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Injectable()
@@ -98,4 +99,41 @@ export class GetMessagesService {
return messageThreads;
}
async getMessagesFromOpportunityId(
workspaceMemberId: string,
opportunityId: string,
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotal> {
const opportunityRepository =
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
'opportunity',
);
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,
page,
pageSize,
);
return messageThreads;
}
}
@@ -41,6 +41,19 @@ class GetTimelineThreadsFromCompanyIdArgs {
pageSize: number;
}
@ArgsType()
class GetTimelineThreadsFromOpportunityIdArgs {
@Field(() => UUIDScalarType)
opportunityId: string;
@Field(() => Int)
page: number;
@Field(() => Int)
@Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
pageSize: number;
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver(() => TimelineThreadsWithTotal)
export class TimelineMessagingResolver {
@@ -100,4 +113,31 @@ export class TimelineMessagingResolver {
return timelineThreads;
}
@Query(() => TimelineThreadsWithTotal)
async getTimelineThreadsFromOpportunityId(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@Args()
{ opportunityId, page, pageSize }: GetTimelineThreadsFromOpportunityIdArgs,
) {
const workspaceMember = await this.userService.loadWorkspaceMember(
user,
workspace,
);
if (!workspaceMember) {
return;
}
const timelineThreads =
await this.getMessagesFromPersonIdsService.getMessagesFromOpportunityId(
workspaceMember.id,
opportunityId,
page,
pageSize,
);
return timelineThreads;
}
}