diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index ed12eda83f..b07d44b765 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -28,6 +28,7 @@ export enum CallRecordingStatus { COMPLETED = 'COMPLETED', FAILED = 'FAILED', JOINING = 'JOINING', + NOT_RECORDED = 'NOT_RECORDED', PROCESSING = 'PROCESSING', RECORDING = 'RECORDING', SCHEDULED = 'SCHEDULED' diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx index 5390b108dd..9fe115d71b 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventCallRecorderAvatar.tsx @@ -3,6 +3,7 @@ import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { useId } from 'react'; import { createPortal } from 'react-dom'; +import { assertUnreachable } from 'twenty-shared/utils'; import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces'; import { CallRecordingStatus } from '~/generated/graphql'; @@ -12,6 +13,11 @@ const IN_PROGRESS_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [ CallRecordingStatus.PROCESSING, ]; +const UNAVAILABLE_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [ + CallRecordingStatus.FAILED, + CallRecordingStatus.NOT_RECORDED, +]; + const getCallRecordingStatusLabel = (status: CallRecordingStatus) => { switch (status) { case CallRecordingStatus.SCHEDULED: @@ -26,6 +32,10 @@ const getCallRecordingStatusLabel = (status: CallRecordingStatus) => { return t`Completed`; case CallRecordingStatus.FAILED: return t`Failed`; + case CallRecordingStatus.NOT_RECORDED: + return t`Not recorded`; + default: + return assertUnreachable(status); } }; @@ -44,12 +54,12 @@ export const CalendarEventCallRecorderAvatar = ({ status, }: CalendarEventCallRecorderAvatarProps) => { const instanceId = useId(); - const hasFailed = status === CallRecordingStatus.FAILED; + const hasNoRecording = UNAVAILABLE_CALL_RECORDING_STATUSES.includes(status); const tooltipAnchorId = `call-recorder-${instanceId.replace(/[^a-zA-Z0-9-_]/g, '-')}`; return ( <> - + { + const isDryRun = options.dryRun ?? false; + + const { flatFieldMetadataMaps, flatObjectMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + 'flatObjectMetadataMaps', + ]); + + const callRecordingObject = + flatObjectMetadataMaps.byUniversalIdentifier[ + CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER + ]; + + if (!isDefined(callRecordingObject)) { + this.logger.log( + `CallRecording object metadata does not exist for workspace ${workspaceId}, skipping`, + ); + + return; + } + + const statusField = + flatFieldMetadataMaps.byUniversalIdentifier[ + CALL_RECORDING_STATUS_FIELD_UNIVERSAL_IDENTIFIER + ]; + + if (!isDefined(statusField)) { + this.logger.log( + `CallRecording status field metadata does not exist for workspace ${workspaceId}, skipping`, + ); + + return; + } + + if (statusField.type !== FieldMetadataType.SELECT) { + throw new Error( + `CallRecording status metadata is not a SELECT field for workspace ${workspaceId}`, + ); + } + + const selectStatusField = + statusField as FlatFieldMetadata; + const currentOptions = selectStatusField.options ?? []; + const hasNotRecordedStatus = currentOptions.some( + (option) => option.value === CallRecordingStatus.NOT_RECORDED, + ); + + if (hasNotRecordedStatus) { + this.logger.log( + `CallRecording status metadata already has NOT_RECORDED for workspace ${workspaceId}, skipping`, + ); + + return; + } + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Adding NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}`, + ); + + if (isDryRun) { + return; + } + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const nextOptionPosition = + currentOptions.reduce( + (highestPosition, option) => Math.max(highestPosition, option.position), + -1, + ) + 1; + + const updatedStatusField: FlatFieldMetadata = { + ...selectStatusField, + options: [ + ...currentOptions, + { ...NOT_RECORDED_STATUS_OPTION, position: nextOptionPosition }, + ], + updatedAt: new Date().toISOString(), + }; + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration( + { + isSystemBuild: true, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + workspaceId, + allFlatEntityOperationByMetadataName: { + fieldMetadata: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [updatedStatusField], + }, + }, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + throw new Error( + `Failed to add NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}: ${JSON.stringify( + validateAndBuildResult, + null, + 2, + )}`, + ); + } + + this.logger.log( + `Added NOT_RECORDED to CallRecording status metadata for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts index 29f25245ef..1df454cc97 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/compute-call-recording-standard-metadata.spec.ts @@ -2,8 +2,10 @@ import { STANDARD_OBJECTS, STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS, } from 'twenty-shared/metadata'; +import { type FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; @@ -36,6 +38,23 @@ describe('CallRecording standard metadata build', () => { expect(callRecording?.isSystem).toBe(true); }); + it('offers the full recording lifecycle as status options', () => { + const statusField = allFlatEntityMaps.flatFieldMetadataMaps + .byUniversalIdentifier[ + STANDARD_OBJECTS.callRecording.fields.status.universalIdentifier + ] as FlatFieldMetadata | undefined; + + expect(statusField?.options?.map((option) => option.value)).toEqual([ + 'SCHEDULED', + 'JOINING', + 'RECORDING', + 'PROCESSING', + 'COMPLETED', + 'FAILED', + 'NOT_RECORDED', + ]); + }); + it('links callRecording to a calendarEvent through a direct relation', () => { const calendarEventField = allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[ diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts index a6728f6065..7a4f5f6554 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util.ts @@ -178,6 +178,13 @@ export const buildCallRecordingStandardFlatFieldMetadatas = ({ position: 5, color: 'gray', }, + { + id: 'cbd14df8-9cc2-4399-92f5-31fc41f3768b', + value: CallRecordingStatus.NOT_RECORDED, + label: i18nLabel(msg`Not recorded`), + position: 6, + color: 'yellow', + }, ], }, standardObjectMetadataRelatedEntityIds, diff --git a/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts b/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts index cc9cff6af5..c03ff8a141 100644 --- a/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts +++ b/packages/twenty-server/src/modules/call-recording/common/enums/call-recording-status.enum.ts @@ -5,4 +5,5 @@ export enum CallRecordingStatus { PROCESSING = 'PROCESSING', COMPLETED = 'COMPLETED', FAILED = 'FAILED', + NOT_RECORDED = 'NOT_RECORDED', }