POC timeline activity (#5697)

TODO: 
- remove WorkspaceIsNotAuditLogged decorators on activity/activityTarget
to log task/note creations
- handle attachments
-  fix css and remove unnecessary styled components or duplicates
This commit is contained in:
Weiko
2024-06-11 18:53:28 +02:00
committed by GitHub
parent 64b8e4ec4d
commit be96c68416
60 changed files with 2134 additions and 443 deletions

View File

@ -1,11 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CalendarBlocklistListener } from 'src/modules/calendar/listeners/calendar-blocklist.listener';
import { CalendarChannelListener } from 'src/modules/calendar/listeners/calendar-channel.listener';
import { CalendarEventParticipantListener } from 'src/modules/calendar/listeners/calendar-event-participant.listener';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
@Module({
imports: [],
providers: [CalendarChannelListener, CalendarBlocklistListener],
imports: [
WorkspaceDataSourceModule,
ObjectMetadataRepositoryModule.forFeature([
TimelineActivityWorkspaceEntity,
]),
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
],
providers: [
CalendarChannelListener,
CalendarBlocklistListener,
CalendarEventParticipantListener,
],
exports: [],
})
export class CalendarModule {}

View File

@ -8,6 +8,8 @@ import { CalendarEventParticipantRepository } from 'src/modules/calendar/reposit
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CalendarCreateCompanyAndContactAfterSyncJobData = {
workspaceId: string;
@ -27,6 +29,8 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
private readonly calendarChannelService: CalendarChannelRepository,
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
async handle(
@ -48,12 +52,24 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
);
}
const { handle, isContactAutoCreationEnabled } = calendarChannels[0];
const { handle, isContactAutoCreationEnabled, connectedAccountId } =
calendarChannels[0];
if (!isContactAutoCreationEnabled || !handle) {
return;
}
const connectedAccount = await this.connectedAccountRepository.getById(
connectedAccountId,
workspaceId,
);
if (!connectedAccount) {
throw new Error(
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
);
}
const calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId =
await this.calendarEventParticipantRepository.getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId(
calendarChannelId,
@ -61,7 +77,7 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
);
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
handle,
connectedAccount,
calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId,
workspaceId,
);

View File

@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
@Injectable()
export class CalendarEventParticipantListener {
constructor(
@InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity)
private readonly timelineActivityRepository: TimelineActivityRepository,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
@OnEvent('calendarEventParticipant.matched')
public async handleCalendarEventParticipantMatchedEvent(payload: {
workspaceId: string;
userId: string;
calendarEventParticipants: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[];
}): Promise<void> {
const calendarEventParticipants = payload.calendarEventParticipants ?? [];
// TODO: move to a job?
const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(
payload.workspaceId,
);
const calendarEventObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'calendarEvent',
workspaceId: payload.workspaceId,
},
});
const calendarEventParticipantsWithPersonId =
calendarEventParticipants.filter((participant) => participant.personId);
if (calendarEventParticipantsWithPersonId.length === 0) {
return;
}
await this.timelineActivityRepository.insertTimelineActivitiesForObject(
'person',
calendarEventParticipantsWithPersonId.map((participant) => ({
dataSourceSchema,
name: 'calendarEvent.linked',
properties: null,
objectName: 'calendarEvent',
recordId: participant.personId,
workspaceMemberId: payload.userId,
workspaceId: payload.workspaceId,
linkedObjectMetadataId: calendarEventObjectMetadata.id,
linkedRecordId: participant.calendarEventId,
linkedRecordCachedName: '',
})),
payload.workspaceId,
);
}
}

View File

@ -51,6 +51,23 @@ export class CalendarEventParticipantRepository {
);
}
public async updateParticipantsPersonIdAndReturn(
participantIds: string[],
personId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`,
[personId, participantIds],
workspaceId,
transactionManager,
);
}
public async updateParticipantsWorkspaceMemberId(
participantIds: string[],
workspaceMemberId: string,

View File

@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EntityManager } from 'typeorm';
@ -22,20 +23,21 @@ export class CalendarEventParticipantService {
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
private readonly personRepository: PersonRepository,
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
private readonly eventEmitter: EventEmitter2,
) {}
public async updateCalendarEventParticipantsAfterPeopleCreation(
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const participants =
await this.calendarEventParticipantRepository.getByHandles(
createdPeople.map((person) => person.email),
workspaceId,
);
if (!participants) return;
if (!participants) return [];
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
@ -57,7 +59,7 @@ export class CalendarEventParticipantService {
}),
);
if (calendarEventParticipantsToUpdate.length === 0) return;
if (calendarEventParticipantsToUpdate.length === 0) return [];
const { flattenedValues, valuesString } =
getFlattenedValuesAndValuesStringForBatchRawQuery(
@ -68,23 +70,26 @@ export class CalendarEventParticipantService {
},
);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" SET "personId" = "data"."personId"
return (
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" SET "personId" = "data"."personId"
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
WHERE "calendarEventParticipant"."id" = "data"."id"`,
flattenedValues,
workspaceId,
transactionManager,
);
WHERE "calendarEventParticipant"."id" = "data"."id"
RETURNING *`,
flattenedValues,
workspaceId,
transactionManager,
)
).flat();
}
public async saveCalendarEventParticipants(
calendarEventParticipants: CalendarEventParticipant[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
if (calendarEventParticipants.length === 0) {
return;
return [];
}
const dataSourceSchema =
@ -111,8 +116,9 @@ export class CalendarEventParticipantService {
},
);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarEventParticipant" ("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus", "personId", "workspaceMemberId") VALUES ${valuesString}`,
return await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarEventParticipant" ("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus", "personId", "workspaceMemberId") VALUES ${valuesString}
RETURNING *`,
flattenedValues,
workspaceId,
transactionManager,
@ -135,11 +141,18 @@ export class CalendarEventParticipantService {
calendarEventParticipantsToUpdate.map((participant) => participant.id);
if (personId) {
await this.calendarEventParticipantRepository.updateParticipantsPersonId(
calendarEventParticipantIdsToUpdate,
personId,
const updatedCalendarEventParticipants =
await this.calendarEventParticipantRepository.updateParticipantsPersonIdAndReturn(
calendarEventParticipantIdsToUpdate,
personId,
workspaceId,
);
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
);
userId: null,
calendarEventParticipants: updatedCalendarEventParticipants,
});
}
if (workspaceMemberId) {
await this.calendarEventParticipantRepository.updateParticipantsWorkspaceMemberId(

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Repository } from 'typeorm';
import { calendar_v3 as calendarV3 } from 'googleapis';
@ -33,9 +34,10 @@ import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decora
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import {
CreateCompanyAndContactJobData,
CreateCompanyAndContactJob,
CreateCompanyAndContactJobData,
} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
@Injectable()
export class GoogleCalendarSyncService {
@ -64,6 +66,7 @@ export class GoogleCalendarSyncService {
private readonly calendarEventParticipantsService: CalendarEventParticipantService,
@InjectMessageQueue(MessageQueue.emailQueue)
private readonly messageQueueService: MessageQueueService,
private readonly eventEmitter: EventEmitter2,
) {}
public async startGoogleCalendarSync(
@ -389,7 +392,7 @@ export class GoogleCalendarSyncService {
eventExternalId: string;
calendarChannelId: string;
}[],
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
calendarChannel: CalendarChannelWorkspaceEntity,
workspaceId: string,
): Promise<void> {
@ -409,8 +412,11 @@ export class GoogleCalendarSyncService {
let startTime: number;
let endTime: number;
const savedCalendarEventParticipantsToEmit: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[] =
[];
try {
dataSourceMetadata?.transaction(async (transactionManager) => {
await dataSourceMetadata?.transaction(async (transactionManager) => {
startTime = Date.now();
await this.calendarEventRepository.saveCalendarEvents(
@ -484,10 +490,15 @@ export class GoogleCalendarSyncService {
startTime = Date.now();
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
participantsToSave,
workspaceId,
transactionManager,
const savedCalendarEventParticipants =
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
participantsToSave,
workspaceId,
transactionManager,
);
savedCalendarEventParticipantsToEmit.push(
...savedCalendarEventParticipants,
);
endTime = Date.now();
@ -499,12 +510,18 @@ export class GoogleCalendarSyncService {
);
});
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
});
if (calendarChannel.isContactAutoCreationEnabled) {
await this.messageQueueService.add<CreateCompanyAndContactJobData>(
CreateCompanyAndContactJob.name,
{
workspaceId,
connectedAccountHandle: connectedAccount.handle,
connectedAccount,
contactsToCreate: participantsToSave,
},
);