Merge command available in exclusion mode but buildHeadlessCommandContextApi returns empty selectedRecords for non-selection modes
https://sonarly.com/issue/23416?type=bug
The merge-multiple-records command throws because `buildHeadlessCommandContextApi` returns an empty `selectedRecords` array when the context store is in "exclusion" mode (Select All), even though the availability expression correctly evaluates `numberOfSelectedRecords >= 2` using a separate state that works for both selection and exclusion modes.
Fix: Added `not isSelectAll` to the merge command's `conditionalAvailabilityExpression` in the standard command menu item constant. This prevents the merge command from appearing in the command menu when records are selected via "Select All" (exclusion mode), because the merge operation requires exact record IDs which are not available in exclusion mode.
This is the same class of bug that was fixed for delete/restore/destroy commands in commit b55765a991 (#19311). The merge command was missed in that fix.
Changes:
1. `standard-command-menu-item.constant.ts` — Prepended `not isSelectAll and` to the merge command's conditional availability expression
2. New file `1-22-workspace-command-1780000002000-fix-merge-command-select-all.command.ts` — Upgrade command to backfill existing workspaces with the corrected expression (follows the exact pattern from the 1-21 select-all fix)
3. `1-22-upgrade-version-command.module.ts` — Registered the new upgrade command
This commit is contained in:
+5
-1
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { FixMergeCommandSelectAllCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000002000-fix-merge-command-select-all.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -15,6 +16,9 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand],
|
||||
providers: [
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
FixMergeCommandSelectAllCommand,
|
||||
],
|
||||
})
|
||||
export class V1_22_UpgradeVersionCommandModule {}
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const UNIVERSAL_IDENTIFIERS_TO_FIX = new Set<string>([
|
||||
STANDARD_COMMAND_MENU_ITEMS.mergeMultipleRecords.universalIdentifier,
|
||||
]);
|
||||
|
||||
@RegisteredWorkspaceCommand('1.22.0', 1780000002000)
|
||||
@Command({
|
||||
name: 'upgrade:1-22:fix-merge-command-select-all',
|
||||
description:
|
||||
'Fix merge command menu item to be unavailable in select-all (exclusion) mode',
|
||||
})
|
||||
export class FixMergeCommandSelectAllCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting merge command select-all fix for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatCommandMenuItemMaps',
|
||||
]);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const itemsToUpdate = [...UNIVERSAL_IDENTIFIERS_TO_FIX]
|
||||
.map((universalIdentifier) => {
|
||||
const standardItem =
|
||||
standardAllFlatEntityMaps.flatCommandMenuItemMaps
|
||||
.byUniversalIdentifier[universalIdentifier];
|
||||
const existingItem =
|
||||
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
|
||||
universalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(standardItem) ||
|
||||
!isDefined(existingItem) ||
|
||||
existingItem.conditionalAvailabilityExpression ===
|
||||
standardItem.conditionalAvailabilityExpression
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingItem,
|
||||
conditionalAvailabilityExpression:
|
||||
standardItem.conditionalAvailabilityExpression,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (itemsToUpdate.length === 0) {
|
||||
this.logger.log(
|
||||
`Merge command menu item expression already up to date for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${itemsToUpdate.length} command menu item(s) to update for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would update ${itemsToUpdate.length} command menu item expression(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: itemsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to fix merge command expression:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to fix merge command menu item expression for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully updated ${itemsToUpdate.length} command menu item expression(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -178,7 +178,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
|
||||
shortLabel: 'Merge',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
conditionalAvailabilityExpression:
|
||||
'numberOfSelectedRecords >= 2 and isDefined(objectMetadataItem.duplicateCriteria) and objectPermissions.canUpdateObjectRecords and objectPermissions.canDestroyObjectRecords and numberOfSelectedRecords <= 9',
|
||||
'not isSelectAll and numberOfSelectedRecords >= 2 and isDefined(objectMetadataItem.duplicateCriteria) and objectPermissions.canUpdateObjectRecords and objectPermissions.canDestroyObjectRecords and numberOfSelectedRecords <= 9',
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
frontComponentUniversalIdentifier: null,
|
||||
engineComponentKey: EngineComponentKey.MERGE_MULTIPLE_RECORDS,
|
||||
|
||||
Reference in New Issue
Block a user