Show call recorders as calendar event participants (#23380)
Closes twentyhq/core-team-issues#2729 Call recordings attached to a calendar event are now displayed next to the human participants, in the timeline event card (`EventCardCalendarEvent`). They are rendered as the source app's `AppChip`, rounded so it sits in the participant avatar group, with the recording status in tooltip https://github.com/user-attachments/assets/e0c393c8-fd65-4468-8c4f-503dd22c13d5 ## Before No call recorder chip displayed TODO: add this in the calendar views (`CalendarEventRow`)
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { CallRecordingStatus } from 'src/modules/call-recording/common/enums/call-recording-status.enum';
|
||||
|
||||
registerEnumType(CallRecordingStatus, {
|
||||
name: 'CallRecordingStatus',
|
||||
description: 'Recording lifecycle status',
|
||||
});
|
||||
|
||||
@ObjectType('TimelineCalendarEventCallRecording')
|
||||
export class TimelineCalendarEventCallRecordingDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => CallRecordingStatus)
|
||||
status: CallRecordingStatus;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
applicationId: string | null;
|
||||
}
|
||||
+4
@@ -2,6 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineCalendarEventCallRecordingDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-call-recording.dto';
|
||||
import { TimelineCalendarEventParticipantDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
|
||||
|
||||
@ObjectType('LinkMetadata')
|
||||
@@ -60,6 +61,9 @@ export class TimelineCalendarEventDTO {
|
||||
@Field(() => [TimelineCalendarEventParticipantDTO])
|
||||
participants: TimelineCalendarEventParticipantDTO[];
|
||||
|
||||
@Field(() => [TimelineCalendarEventCallRecordingDTO])
|
||||
callRecordings: TimelineCalendarEventCallRecordingDTO[];
|
||||
|
||||
@Field(() => CalendarChannelVisibility)
|
||||
visibility: CalendarChannelVisibility;
|
||||
}
|
||||
|
||||
+9
@@ -31,6 +31,7 @@ describe('TimelineCalendarEventService', () => {
|
||||
let mockUserWorkspaceRepository: { findOne: jest.Mock };
|
||||
let mockWorkspaceMemberRepository: { findOne: jest.Mock };
|
||||
let mockFileUrlService: { signFirstFilesFieldFileUrl: jest.Mock };
|
||||
let mockCallRecordingRepository: { find: jest.Mock };
|
||||
|
||||
const mockCalendarEvent: Partial<CalendarEventWorkspaceEntity> = {
|
||||
id: '1',
|
||||
@@ -69,6 +70,10 @@ describe('TimelineCalendarEventService', () => {
|
||||
signFirstFilesFieldFileUrl: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
mockCallRecordingRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const mockGlobalWorkspaceOrmManager = {
|
||||
getRepository: jest
|
||||
.fn()
|
||||
@@ -77,6 +82,10 @@ describe('TimelineCalendarEventService', () => {
|
||||
return Promise.resolve(mockWorkspaceMemberRepository);
|
||||
}
|
||||
|
||||
if (entityName === 'callRecording') {
|
||||
return Promise.resolve(mockCallRecordingRepository);
|
||||
}
|
||||
|
||||
return Promise.resolve(mockCalendarEventRepository);
|
||||
}),
|
||||
executeInWorkspaceContext: jest
|
||||
|
||||
+49
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import omit from 'lodash.omit';
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Any, In, type Repository } from 'typeorm';
|
||||
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
@@ -16,6 +17,8 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
|
||||
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 CallRecordingStatus } from 'src/modules/call-recording/common/enums/call-recording-status.enum';
|
||||
import { type CallRecordingWorkspaceEntity } from 'src/modules/call-recording/standard-objects/call-recording.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -107,6 +110,50 @@ export class TimelineCalendarEventService {
|
||||
},
|
||||
});
|
||||
|
||||
const callRecordingRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CallRecordingWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'callRecording',
|
||||
);
|
||||
|
||||
const callRecordings = await callRecordingRepository.find({
|
||||
where: {
|
||||
calendarEventId: Any(ids),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
applicationId: true,
|
||||
calendarEventId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const callRecordingsByCalendarEventId = callRecordings.reduce<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
status: CallRecordingStatus;
|
||||
applicationId: string | null;
|
||||
}[]
|
||||
>
|
||||
>((acc, callRecording) => {
|
||||
if (!isDefined(callRecording.calendarEventId)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const existing = acc.get(callRecording.calendarEventId) ?? [];
|
||||
|
||||
existing.push({
|
||||
id: callRecording.id,
|
||||
status: callRecording.status,
|
||||
applicationId: callRecording.applicationId ?? null,
|
||||
});
|
||||
acc.set(callRecording.calendarEventId, existing);
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const allCalendarChannelIds = [
|
||||
...new Set(
|
||||
events.flatMap((event) =>
|
||||
@@ -259,6 +306,8 @@ export class TimelineCalendarEventService {
|
||||
startsAt: event.startsAt as unknown as Date,
|
||||
endsAt: event.endsAt as unknown as Date,
|
||||
participants,
|
||||
callRecordings:
|
||||
callRecordingsByCalendarEventId.get(event.id) ?? [],
|
||||
visibility,
|
||||
location: event.location ?? '',
|
||||
conferenceSolution: event.conferenceSolution ?? '',
|
||||
|
||||
Reference in New Issue
Block a user