Stop building the Campaigns navigation menu item (#23493)

Campaigns show up in the sidebar of every workspace, including brand new
ones, while the feature is still behind `IS_EMAIL_GROUP_ENABLED`.

## Why it leaked

`navigationMenuItem` has no `conditionalAvailabilityExpression` column.
Only two entities do:

- `page-layout-widget.entity.ts`
- `command-menu-item.entity.ts`

So there was no flag to attach. #23188 gated every surface that supports
gating — the campaign command menu items carry
`featureFlags.IS_EMAIL_GROUP_ENABLED`
(`standard-command-menu-item.constant.ts:720` and `:735`) and the page
layout widgets are gated the same way — but `allMessageCampaigns` was
added to `FLAT_NAVIGATION_MENU_ITEM_NAMES`, which builds
unconditionally.

`messageCampaign` is `isSystem: true`, so the navigation item was the
only thing exposing the feature.

## Change

- Drop `allMessageCampaigns` from `FLAT_NAVIGATION_MENU_ITEM_NAMES`. Its
definition stays in `STANDARD_NAVIGATION_MENU_ITEMS` so the identifier
remains reserved and re-enabling is a one-line change.
- Add workspace command `1785324390000` to delete the rows from
workspaces already provisioned with the item. It collects every matching
row rather than looking the identifier up once, because each user
workspace gets its own.

## Trade-off

Campaigns become unreachable from the sidebar even with the flag on,
which restores the pre-#23188 state. The durable fix is adding
`conditionalAvailabilityExpression` to `navigationMenuItem` so the item
can be gated like the command menu items — a schema change, deliberately
not done here.

## Verification

Reproduced on a fresh `database:reset` against `main`: two
`20202020-b00b-4b0b-8b0b-c0aba11c000b` rows (type `OBJECT`, position 7)
for the single seeded workspace, structurally identical to the other nav
items. Typecheck, `oxfmt --check src/` and `oxlint --type-aware src/`
clean on this branch. The post-fix database check did not complete —
local Postgres died mid-reset — so the removal is verified by
construction and by the build-list change, not yet by a second reset.


---
_Generated by [Claude
Code](https://claude.ai/code/session_01NZKeMSUBG3VQPuoy8i5Awe)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23493?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:
Félix Malfait
2026-07-29 16:08:27 +02:00
committed by GitHub
parent 63d34e0133
commit e79424ddb0
3 changed files with 110 additions and 1 deletions
@@ -6,6 +6,7 @@ import { BackfillMessageListMembersJunctionTargetCommand } from 'src/database/co
import { AddMessageCampaignComposerTabCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229940000-add-message-campaign-composer-tab.command';
import { ConfigureMessageCampaignCommandMenuCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229960000-configure-message-campaign-command-menu.command';
import { AddMessageCampaignNameFieldCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229970000-add-message-campaign-name-field.command';
import { RemoveMessageCampaignNavigationMenuItemCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785332550000-remove-message-campaign-navigation-menu-item.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -26,6 +27,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
AddMessageCampaignComposerTabCommand,
ConfigureMessageCampaignCommandMenuCommand,
AddMessageCampaignNameFieldCommand,
RemoveMessageCampaignNavigationMenuItemCommand,
],
})
export class V2_25_UpgradeVersionCommandModule {}
@@ -0,0 +1,108 @@
import { Command } from 'nest-commander';
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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const ALL_MESSAGE_CAMPAIGNS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
STANDARD_NAVIGATION_MENU_ITEMS.allMessageCampaigns.universalIdentifier;
@RegisteredWorkspaceCommand('2.25.0', 1785332550000)
@Command({
name: 'upgrade:2-25:remove-message-campaign-navigation-menu-item',
description:
'Remove the Campaigns navigation menu item from workspaces provisioned while it was built unconditionally, since navigation items cannot be gated behind IS_EMAIL_GROUP_ENABLED yet',
})
export class RemoveMessageCampaignNavigationMenuItemCommand 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 { flatNavigationMenuItemMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatNavigationMenuItemMaps',
]);
// Every user workspace gets its own row, so collect them all rather than
// looking the identifier up once.
const navigationMenuItemsToDelete = Object.values(
flatNavigationMenuItemMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(navigationMenuItem) =>
navigationMenuItem.universalIdentifier ===
ALL_MESSAGE_CAMPAIGNS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
);
if (navigationMenuItemsToDelete.length === 0) {
this.logger.log(
`Campaigns navigation menu item not present for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Removing ${navigationMenuItemsToDelete.length} Campaigns navigation menu item(s) for workspace ${workspaceId}`,
);
if (isDryRun) {
return;
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
allFlatEntityOperationByMetadataName: {
navigationMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: navigationMenuItemsToDelete,
flatEntityToUpdate: [],
},
},
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to remove the Campaigns navigation menu item:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to remove the Campaigns navigation menu item for workspace ${workspaceId}`,
);
}
this.logger.log(
`Removed the Campaigns navigation menu item for workspace ${workspaceId}`,
);
}
}
@@ -15,7 +15,6 @@ import {
const FLAT_NAVIGATION_MENU_ITEM_NAMES = [
'allCompanies',
'allDashboards',
'allMessageCampaigns',
'allNotes',
'allOpportunities',
'allPeople',