[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',
|
COMPLETED = 'COMPLETED',
|
||||||
FAILED = 'FAILED',
|
FAILED = 'FAILED',
|
||||||
JOINING = 'JOINING',
|
JOINING = 'JOINING',
|
||||||
|
NOT_RECORDED = 'NOT_RECORDED',
|
||||||
PROCESSING = 'PROCESSING',
|
PROCESSING = 'PROCESSING',
|
||||||
RECORDING = 'RECORDING',
|
RECORDING = 'RECORDING',
|
||||||
SCHEDULED = 'SCHEDULED'
|
SCHEDULED = 'SCHEDULED'
|
||||||
|
|||||||
+12
-2
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { assertUnreachable } from 'twenty-shared/utils';
|
||||||
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces';
|
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces';
|
||||||
import { CallRecordingStatus } from '~/generated/graphql';
|
import { CallRecordingStatus } from '~/generated/graphql';
|
||||||
|
|
||||||
@@ -12,6 +13,11 @@ const IN_PROGRESS_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [
|
|||||||
CallRecordingStatus.PROCESSING,
|
CallRecordingStatus.PROCESSING,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const UNAVAILABLE_CALL_RECORDING_STATUSES: CallRecordingStatus[] = [
|
||||||
|
CallRecordingStatus.FAILED,
|
||||||
|
CallRecordingStatus.NOT_RECORDED,
|
||||||
|
];
|
||||||
|
|
||||||
const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
|
const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CallRecordingStatus.SCHEDULED:
|
case CallRecordingStatus.SCHEDULED:
|
||||||
@@ -26,6 +32,10 @@ const getCallRecordingStatusLabel = (status: CallRecordingStatus) => {
|
|||||||
return t`Completed`;
|
return t`Completed`;
|
||||||
case CallRecordingStatus.FAILED:
|
case CallRecordingStatus.FAILED:
|
||||||
return t`Failed`;
|
return t`Failed`;
|
||||||
|
case CallRecordingStatus.NOT_RECORDED:
|
||||||
|
return t`Not recorded`;
|
||||||
|
default:
|
||||||
|
return assertUnreachable(status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,12 +54,12 @@ export const CalendarEventCallRecorderAvatar = ({
|
|||||||
status,
|
status,
|
||||||
}: CalendarEventCallRecorderAvatarProps) => {
|
}: CalendarEventCallRecorderAvatarProps) => {
|
||||||
const instanceId = useId();
|
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, '-')}`;
|
const tooltipAnchorId = `call-recorder-${instanceId.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledContainer id={tooltipAnchorId} isDisabled={hasFailed}>
|
<StyledContainer id={tooltipAnchorId} isDisabled={hasNoRecording}>
|
||||||
<AppChip
|
<AppChip
|
||||||
applicationId={applicationId}
|
applicationId={applicationId}
|
||||||
fallbackApplicationData={{ name: t`Call recorder` }}
|
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 { 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 { 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 { 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 { 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 { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
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';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ViewEntity]),
|
TypeOrmModule.forFeature([FieldMetadataEntity, ViewEntity]),
|
||||||
ApplicationModule,
|
ApplicationModule,
|
||||||
WorkspaceCacheModule,
|
WorkspaceCacheModule,
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
@@ -22,6 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
|||||||
providers: [
|
providers: [
|
||||||
ReconcileIndexViewUniversalIdentifierCommand,
|
ReconcileIndexViewUniversalIdentifierCommand,
|
||||||
DemoteAndBackfillApplicationIndexViewCommand,
|
DemoteAndBackfillApplicationIndexViewCommand,
|
||||||
|
AddNotRecordedCallRecordingStatusCommand,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class V2_26_UpgradeVersionCommandModule {}
|
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_OBJECTS,
|
||||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||||
} from 'twenty-shared/metadata';
|
} from 'twenty-shared/metadata';
|
||||||
|
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
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 { 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';
|
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);
|
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', () => {
|
it('links callRecording to a calendarEvent through a direct relation', () => {
|
||||||
const calendarEventField =
|
const calendarEventField =
|
||||||
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||||
|
|||||||
+7
@@ -178,6 +178,13 @@ export const buildCallRecordingStandardFlatFieldMetadatas = ({
|
|||||||
position: 5,
|
position: 5,
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'cbd14df8-9cc2-4399-92f5-31fc41f3768b',
|
||||||
|
value: CallRecordingStatus.NOT_RECORDED,
|
||||||
|
label: i18nLabel(msg`Not recorded`),
|
||||||
|
position: 6,
|
||||||
|
color: 'yellow',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
standardObjectMetadataRelatedEntityIds,
|
standardObjectMetadataRelatedEntityIds,
|
||||||
|
|||||||
+1
@@ -5,4 +5,5 @@ export enum CallRecordingStatus {
|
|||||||
PROCESSING = 'PROCESSING',
|
PROCESSING = 'PROCESSING',
|
||||||
COMPLETED = 'COMPLETED',
|
COMPLETED = 'COMPLETED',
|
||||||
FAILED = 'FAILED',
|
FAILED = 'FAILED',
|
||||||
|
NOT_RECORDED = 'NOT_RECORDED',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user