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:
+2
@@ -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 {}
|
||||
|
||||
+108
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-1
@@ -15,7 +15,6 @@ import {
|
||||
const FLAT_NAVIGATION_MENU_ITEM_NAMES = [
|
||||
'allCompanies',
|
||||
'allDashboards',
|
||||
'allMessageCampaigns',
|
||||
'allNotes',
|
||||
'allOpportunities',
|
||||
'allPeople',
|
||||
|
||||
Reference in New Issue
Block a user