From f86bf637d1097a75d339222c82f67c86cded65e7 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:43:36 +0530 Subject: [PATCH] [BREAKING CHANGE] remove call recording feature flag and backfill upgrade command for existing command menu items navigation command (#22176) Review in cubic --- .../src/metadata/generated/schema.graphql | 1 - .../src/metadata/generated/schema.ts | 3 +- .../src/generated-admin/graphql.ts | 1 - .../src/generated-metadata/graphql.ts | 1 - .../2-17-upgrade-version-command.module.ts | 20 ++ ...nu-item-availability-expression.command.ts | 100 +++++++++ ...ty-expression-sync-operations.util.spec.ts | 210 ++++++++++++++++++ ...ability-expression-sync-operations.util.ts | 69 ++++++ .../workspace-command-provider.module.ts | 2 + ...gation-flat-command-menu-item.util.spec.ts | 22 +- ...-navigation-flat-command-menu-item.util.ts | 2 - .../workspace-entity-manager.spec.ts | 1 - .../twenty-shared/src/types/FeatureFlagKey.ts | 1 - 13 files changed, 414 insertions(+), 19 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/__tests__/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.spec.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index ed335e3a21..94d3fa30a1 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1777,7 +1777,6 @@ enum FeatureFlagKey { IS_REST_METADATA_API_NEW_FORMAT_DIRECT IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED IS_SETTINGS_DISCOVERY_HERO_ENABLED - IS_CALL_RECORDING_ENABLED IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED } diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 478dfb24b4..8846e485d8 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1409,7 +1409,7 @@ export interface FeatureFlag { __typename: 'FeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_CALL_RECORDING_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED' export interface WorkspaceUrls { customUrl?: Scalars['String'] @@ -9090,7 +9090,6 @@ export const enumFeatureFlagKey = { IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const, IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' as const, IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const, - IS_CALL_RECORDING_ENABLED: 'IS_CALL_RECORDING_ENABLED' as const, IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED: 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED' as const } diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index 66f99241e8..508d70fa56 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -297,7 +297,6 @@ export type FeatureFlag = { }; export enum FeatureFlagKey { - IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED', IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index b241e072e5..e12484419d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1708,7 +1708,6 @@ export type FeatureFlag = { }; export enum FeatureFlagKey { - IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED', IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module.ts new file mode 100644 index 0000000000..16a103ca87 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; + +import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; +import { SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; + +@Module({ + imports: [ + ApplicationModule, + WorkspaceCacheModule, + WorkspaceIteratorModule, + WorkspaceMigrationModule, + ], + providers: [ + SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand, + ], +}) +export class V2_17_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command.ts new file mode 100644 index 0000000000..5b2a3ff491 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command.ts @@ -0,0 +1,100 @@ +import { Command } from 'nest-commander'; + +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-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 { buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations } from 'src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; +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'; + +@RegisteredWorkspaceCommand('2.17.0', 1801000000000) +@Command({ + name: 'upgrade:2-17:sync-call-recording-navigation-command-menu-item-availability-expression', + description: + 'Remove the retired call recording feature-flag gate from existing CallRecording navigation command menu items', +}) +export class SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand extends ActiveOrSuspendedWorkspaceCommandRunner { + 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 { + const isDryRun = options.dryRun ?? false; + + const { flatCommandMenuItemMaps, flatObjectMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatCommandMenuItemMaps', + 'flatObjectMetadataMaps', + ]); + + const commandMenuItemOperations = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: flatCommandMenuItemMaps, + existingFlatObjectMetadataMaps: flatObjectMetadataMaps, + now: new Date().toISOString(), + }, + ); + + const commandMenuItemsToUpdate = + commandMenuItemOperations.flatEntityToUpdate; + + if (commandMenuItemsToUpdate.length === 0) { + this.logger.log( + `CallRecording navigation command menu item availability expression already synced for workspace ${workspaceId}, skipping`, + ); + + return; + } + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Syncing ${commandMenuItemsToUpdate.length} CallRecording navigation command menu item availability expression(s) for workspace ${workspaceId}`, + ); + + if (isDryRun) { + return; + } + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + isSystemBuild: true, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + allFlatEntityOperationByMetadataName: { + commandMenuItem: commandMenuItemOperations, + }, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to sync CallRecording navigation command menu item availability expression:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + + throw new Error( + `Failed to sync CallRecording navigation command menu item availability expression for workspace ${workspaceId}`, + ); + } + + this.logger.log( + `Successfully synced ${commandMenuItemsToUpdate.length} CallRecording navigation command menu item availability expression(s) for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/__tests__/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.spec.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/__tests__/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.spec.ts new file mode 100644 index 0000000000..980d61ce45 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/__tests__/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.spec.ts @@ -0,0 +1,210 @@ +import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application'; +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; + +import { + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations, + CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, +} from 'src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util'; +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +const APPLICATION_ID = 'application-id'; +const WORKSPACE_ID = 'workspace-id'; +const CREATED_AT = '2026-06-01T00:00:00.000Z'; +const NOW = '2026-06-25T00:00:00.000Z'; + +const buildFlatCommandMenuItemMaps = ( + flatCommandMenuItems: FlatCommandMenuItem[], +): FlatEntityMaps => ({ + byUniversalIdentifier: Object.fromEntries( + flatCommandMenuItems.map((flatCommandMenuItem) => [ + flatCommandMenuItem.universalIdentifier, + flatCommandMenuItem, + ]), + ), + universalIdentifierById: Object.fromEntries( + flatCommandMenuItems.map((flatCommandMenuItem) => [ + flatCommandMenuItem.id, + flatCommandMenuItem.universalIdentifier, + ]), + ), + universalIdentifiersByApplicationId: { + [APPLICATION_ID]: flatCommandMenuItems.map( + (flatCommandMenuItem) => flatCommandMenuItem.universalIdentifier, + ), + }, +}); + +const buildFlatObjectMetadataMaps = ( + flatObjectMetadatas: FlatObjectMetadata[], +): FlatEntityMaps => ({ + byUniversalIdentifier: Object.fromEntries( + flatObjectMetadatas.map((flatObjectMetadata) => [ + flatObjectMetadata.universalIdentifier, + flatObjectMetadata, + ]), + ), + universalIdentifierById: Object.fromEntries( + flatObjectMetadatas.map((flatObjectMetadata) => [ + flatObjectMetadata.id, + flatObjectMetadata.universalIdentifier, + ]), + ), + universalIdentifiersByApplicationId: { + [APPLICATION_ID]: flatObjectMetadatas.map( + (flatObjectMetadata) => flatObjectMetadata.universalIdentifier, + ), + }, +}); + +const buildCallRecordingObjectMetadataMaps = () => + buildFlatObjectMetadataMaps([ + getFlatObjectMetadataMock({ + id: 'call-recording-object-metadata-id', + universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier, + nameSingular: 'callRecording', + namePlural: 'callRecordings', + applicationId: APPLICATION_ID, + applicationUniversalIdentifier: + TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, + workspaceId: WORKSPACE_ID, + }), + ]); + +const buildCallRecordingNavigationCommandMenuItem = ({ + conditionalAvailabilityExpression, +}: { + conditionalAvailabilityExpression: string | null; +}): FlatCommandMenuItem => ({ + ...buildNavigationFlatCommandMenuItem({ + objectMetadata: { + id: 'call-recording-object-metadata-id', + universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier, + nameSingular: 'callRecording', + shortcut: null, + }, + commandMenuItemId: 'call-recording-navigation-command-menu-item-id', + applicationId: APPLICATION_ID, + applicationUniversalIdentifier: + TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, + workspaceId: WORKSPACE_ID, + position: 0, + now: CREATED_AT, + }), + conditionalAvailabilityExpression, +}); + +describe('buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations', () => { + it('rewrites the legacy call recording feature-flag gate to the read-permission gate', () => { + const legacyCallRecordingNavigationCommandMenuItem = + buildCallRecordingNavigationCommandMenuItem({ + conditionalAvailabilityExpression: + LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + }); + + const result = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([ + legacyCallRecordingNavigationCommandMenuItem, + ]), + existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(), + now: NOW, + }, + ); + + expect(result.flatEntityToCreate).toHaveLength(0); + expect(result.flatEntityToDelete).toHaveLength(0); + expect(result.flatEntityToUpdate).toHaveLength(1); + expect(result.flatEntityToUpdate[0]).toMatchObject({ + id: legacyCallRecordingNavigationCommandMenuItem.id, + conditionalAvailabilityExpression: + CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + updatedAt: NOW, + }); + }); + + it('does not update the command menu item when the expression is already synced', () => { + const syncedCallRecordingNavigationCommandMenuItem = + buildCallRecordingNavigationCommandMenuItem({ + conditionalAvailabilityExpression: + CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + }); + + const result = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([ + syncedCallRecordingNavigationCommandMenuItem, + ]), + existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(), + now: NOW, + }, + ); + + expect(result.flatEntityToUpdate).toHaveLength(0); + }); + + it('does not update unrelated command menu item availability expressions', () => { + const customCallRecordingNavigationCommandMenuItem = + buildCallRecordingNavigationCommandMenuItem({ + conditionalAvailabilityExpression: + 'targetObjectReadPermissions.callRecording and permissionFlags.DATA_MODEL', + }); + + const result = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([ + customCallRecordingNavigationCommandMenuItem, + ]), + existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(), + now: NOW, + }, + ); + + expect(result.flatEntityToUpdate).toHaveLength(0); + }); + + it('does not update when the call recording object metadata is missing', () => { + const legacyCallRecordingNavigationCommandMenuItem = + buildCallRecordingNavigationCommandMenuItem({ + conditionalAvailabilityExpression: + LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + }); + + const result = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([ + legacyCallRecordingNavigationCommandMenuItem, + ]), + existingFlatObjectMetadataMaps: buildFlatObjectMetadataMaps([]), + now: NOW, + }, + ); + + expect(result.flatEntityToUpdate).toHaveLength(0); + }); + + it('does not update when the call recording navigation command menu item is missing', () => { + const result = + buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations( + { + existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]), + existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(), + now: NOW, + }, + ); + + expect(result).toEqual({ + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }); + }); +}); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.ts new file mode 100644 index 0000000000..4cf2b7863e --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util.ts @@ -0,0 +1,69 @@ +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; +import { v5 } from 'uuid'; + +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { NAVIGATION_COMMAND_UUID_NAMESPACE } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatEntityToCreateDeleteUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-to-create-delete-update.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION = + 'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording'; + +export const CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION = + 'targetObjectReadPermissions.callRecording'; + +const CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER = + STANDARD_OBJECTS.callRecording.universalIdentifier; + +const CALL_RECORDING_NAVIGATION_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER = v5( + CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER, + NAVIGATION_COMMAND_UUID_NAMESPACE, +); + +export const buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations = + ({ + existingFlatCommandMenuItemMaps, + existingFlatObjectMetadataMaps, + now, + }: { + existingFlatCommandMenuItemMaps: FlatEntityMaps; + existingFlatObjectMetadataMaps: FlatEntityMaps; + now: string; + }): FlatEntityToCreateDeleteUpdate<'commandMenuItem'> => { + const existingCallRecordingObjectMetadata = + existingFlatObjectMetadataMaps.byUniversalIdentifier[ + CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER + ]; + const existingCallRecordingNavigationCommandMenuItem = + existingFlatCommandMenuItemMaps.byUniversalIdentifier[ + CALL_RECORDING_NAVIGATION_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER + ]; + + if ( + !isDefined(existingCallRecordingObjectMetadata) || + !isDefined(existingCallRecordingNavigationCommandMenuItem) || + existingCallRecordingNavigationCommandMenuItem.conditionalAvailabilityExpression !== + LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION + ) { + return { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }; + } + + return { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + { + ...existingCallRecordingNavigationCommandMenuItem, + conditionalAvailabilityExpression: + CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION, + updatedAt: now, + }, + ], + }; + }; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts index a2ad31f971..33112e8e57 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts @@ -17,6 +17,7 @@ import { V2_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade import { V2_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-14/2-14-upgrade-version-command.module'; import { V2_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-15/2-15-upgrade-version-command.module'; import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-16/2-16-upgrade-version-command.module'; +import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade V2_14_UpgradeVersionCommandModule, V2_15_UpgradeVersionCommandModule, V2_16_UpgradeVersionCommandModule, + V2_17_UpgradeVersionCommandModule, ], }) export class WorkspaceCommandProviderModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts index 7f0279d217..5fbe8c5472 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/__tests__/build-navigation-flat-command-menu-item.util.spec.ts @@ -119,13 +119,14 @@ describe('buildNavigationFlatCommandMenuItem', () => { ...baseArgs, objectMetadata: { ...baseObjectMetadata, - universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier, - nameSingular: 'callRecording', + universalIdentifier: + STANDARD_OBJECTS.messageCampaign.universalIdentifier, + nameSingular: 'messageCampaign', }, }); expect(result.conditionalAvailabilityExpression).toBe( - 'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording', + 'featureFlags.IS_EMAIL_GROUP_ENABLED and targetObjectReadPermissions.messageCampaign', ); }); @@ -154,14 +155,15 @@ describe('buildNavigationFlatCommandMenuItem', () => { }); describe('buildNavigationConditionalAvailabilityExpression', () => { - it('gates the standard call recording object behind both the flag and read permission', () => { + it('gates a feature-flagged standard object behind both the flag and read permission', () => { expect( buildNavigationConditionalAvailabilityExpression({ - universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier, - nameSingular: 'callRecording', + universalIdentifier: + STANDARD_OBJECTS.messageCampaign.universalIdentifier, + nameSingular: 'messageCampaign', }), ).toBe( - 'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording', + 'featureFlags.IS_EMAIL_GROUP_ENABLED and targetObjectReadPermissions.messageCampaign', ); }); @@ -174,12 +176,12 @@ describe('buildNavigationConditionalAvailabilityExpression', () => { ).toBe('targetObjectReadPermissions.person'); }); - it('does not gate a custom object that reuses the callRecording name', () => { + it('does not gate a custom object that reuses a feature-flagged object name', () => { expect( buildNavigationConditionalAvailabilityExpression({ universalIdentifier: 'custom-object-universal-id', - nameSingular: 'callRecording', + nameSingular: 'messageCampaign', }), - ).toBe('targetObjectReadPermissions.callRecording'); + ).toBe('targetObjectReadPermissions.messageCampaign'); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts index 8120c89dfe..e2726a68ff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util.ts @@ -20,8 +20,6 @@ export const NAVIGATION_INTERPOLATED_ICON = const NAVIGATION_FEATURE_FLAG_GATE_BY_OBJECT_UNIVERSAL_IDENTIFIER: Partial< Record > = { - [STANDARD_OBJECTS.callRecording.universalIdentifier]: - FeatureFlagKey.IS_CALL_RECORDING_ENABLED, [STANDARD_OBJECTS.messageCampaign.universalIdentifier]: FeatureFlagKey.IS_EMAIL_GROUP_ENABLED, [STANDARD_OBJECTS.messageList.universalIdentifier]: diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 14e88d51ae..cb697ae7ba 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -240,7 +240,6 @@ describe('WorkspaceEntityManager', () => { IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false, IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: false, IS_SETTINGS_DISCOVERY_HERO_ENABLED: false, - IS_CALL_RECORDING_ENABLED: false, IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED: false, }, userWorkspaceRoleMap: {}, diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index 5f77201357..356b38b573 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -7,6 +7,5 @@ export enum FeatureFlagKey { IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT', IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED', IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED', - IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED', IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED = 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED', }