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)
---
.../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',
}