[BREAKING-CHANGE] Add NOT_RECORDED call recording status (#23478)
Adds NOT_RECORDED to the CallRecording status select, for meetings where nothing was captured (bot never admitted, meeting not started, nobody joined). First part of twentyhq/core-team-issues#2706, split out so existing workspaces are upgraded before the call-recorder app starts writing the new status. - NOT_RECORDED enum value + standard select option - 2-26 workspace upgrade command adding the option to existing workspaces (idempotent, same option id as the standard definition) App-side classification from Recall sub codes comes in a follow-up PR. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23478?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -28,6 +28,7 @@ export enum CallRecordingStatus {
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
JOINING = 'JOINING',
|
||||
NOT_RECORDED = 'NOT_RECORDED',
|
||||
PROCESSING = 'PROCESSING',
|
||||
RECORDING = 'RECORDING',
|
||||
SCHEDULED = 'SCHEDULED'
|
||||
|
||||
+12
-2
@@ -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 (
|
||||
<>
|
||||
<StyledContainer id={tooltipAnchorId} isDisabled={hasFailed}>
|
||||
<StyledContainer id={tooltipAnchorId} isDisabled={hasNoRecording}>
|
||||
<AppChip
|
||||
applicationId={applicationId}
|
||||
fallbackApplicationData={{ name: t`Call recorder` }}
|
||||
|
||||
+4
-1
@@ -4,7 +4,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { ReconcileIndexViewUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255689000-reconcile-index-view-universal-identifier.command';
|
||||
import { DemoteAndBackfillApplicationIndexViewCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785255690000-demote-and-backfill-application-index-view.command';
|
||||
import { AddNotRecordedCallRecordingStatusCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-workspace-command-1785334800000-add-not-recorded-call-recording-status.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
@@ -12,7 +14,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ViewEntity]),
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity, ViewEntity]),
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
@@ -22,6 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
providers: [
|
||||
ReconcileIndexViewUniversalIdentifierCommand,
|
||||
DemoteAndBackfillApplicationIndexViewCommand,
|
||||
AddNotRecordedCallRecordingStatusCommand,
|
||||
],
|
||||
})
|
||||
export class V2_26_UpgradeVersionCommandModule {}
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { CallRecordingStatus } from 'src/modules/call-recording/common/enums/call-recording-status.enum';
|
||||
|
||||
const CALL_RECORDING_STATUS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.callRecording.fields.status.universalIdentifier;
|
||||
const CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.callRecording.universalIdentifier;
|
||||
|
||||
// Same option id as the standard definition so provisioned and upgraded workspaces match.
|
||||
const NOT_RECORDED_STATUS_OPTION: FieldMetadataComplexOption = {
|
||||
id: 'cbd14df8-9cc2-4399-92f5-31fc41f3768b',
|
||||
value: CallRecordingStatus.NOT_RECORDED,
|
||||
label: 'Not recorded',
|
||||
position: 6,
|
||||
color: 'yellow',
|
||||
};
|
||||
|
||||
@RegisteredWorkspaceCommand('2.26.0', 1785334800000)
|
||||
@Command({
|
||||
name: 'upgrade:2-26:add-not-recorded-call-recording-status',
|
||||
description:
|
||||
'Add the NOT_RECORDED option to the CallRecording status select in existing workspaces',
|
||||
})
|
||||
export class AddNotRecordedCallRecordingStatusCommand extends ProvisionedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
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<FieldMetadataType.SELECT>;
|
||||
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<FieldMetadataType.SELECT> = {
|
||||
...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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
@@ -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<FieldMetadataType.SELECT> | 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[
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+1
@@ -5,4 +5,5 @@ export enum CallRecordingStatus {
|
||||
PROCESSING = 'PROCESSING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
NOT_RECORDED = 'NOT_RECORDED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user