Fix delete/restore/destroy commands unavailable in select-all mode (#19311)

Fixes https://github.com/twentyhq/twenty/issues/19309

In exclusion mode (select all), selectedRecords is always [] since
individual record IDs aren't tracked. The noneDefined()/everyDefined()
checks return false on empty arrays by design, which hides the delete,
restore, and destroy commands from the command menu.

- Wrap selectedRecords array checks with (isSelectAll or ...) to bypass
when in exclusion mode
- Remove the `numberOfSelectedRecords < 10000` limit
- Add `upgrade:1-21:fix-select-all-command-menu-items` command to
backfill existing workspaces
- Add tests
This commit is contained in:
Raphaël Bosi
2026-04-03 17:03:45 +02:00
committed by GitHub
parent 0b8421a45a
commit b55765a991
6 changed files with 230 additions and 8 deletions
@@ -307,6 +307,81 @@ describe('evaluateConditionalAvailabilityExpression', () => {
});
});
describe('isSelectAll bypasses selectedRecords array checks', () => {
it('should return true for "isSelectAll or noneDefined(...)" when isSelectAll is true and selectedRecords is empty', () => {
const context = buildContext({
isSelectAll: true,
selectedRecords: [],
});
expect(
evaluateConditionalAvailabilityExpression(
'isSelectAll or noneDefined(selectedRecords, "deletedAt")',
context,
),
).toBe(true);
});
it('should return true for "isSelectAll or everyDefined(...)" when isSelectAll is true and selectedRecords is empty', () => {
const context = buildContext({
isSelectAll: true,
selectedRecords: [],
});
expect(
evaluateConditionalAvailabilityExpression(
'isSelectAll or everyDefined(selectedRecords, "deletedAt")',
context,
),
).toBe(true);
});
it('should evaluate the full deleteRecords expression to true in select-all mode', () => {
const expression =
'numberOfSelectedRecords >= 1 and not hasAnySoftDeleteFilterOnView and objectPermissions.canSoftDeleteObjectRecords and (isSelectAll or noneDefined(selectedRecords, "deletedAt"))';
const context = buildContext({
isSelectAll: true,
selectedRecords: [],
numberOfSelectedRecords: 50,
hasAnySoftDeleteFilterOnView: false,
objectPermissions: {
objectMetadataId: '',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: false,
restrictedFields: {},
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
},
});
expect(
evaluateConditionalAvailabilityExpression(expression, context),
).toBe(true);
});
it('should still respect noneDefined when isSelectAll is false and records are present', () => {
const expression =
'isSelectAll or noneDefined(selectedRecords, "deletedAt")';
const contextWithDeletedRecord = buildContext({
isSelectAll: false,
selectedRecords: [
{ id: 'id-1', createdAt: '', updatedAt: '', deletedAt: '2024-01-01' },
],
});
expect(
evaluateConditionalAvailabilityExpression(
expression,
contextWithDeletedRecord,
),
).toBe(false);
});
});
describe('activateWorkflow expression regression', () => {
const activateWorkflowExpression =
'everyDefined(selectedRecords, "currentVersion.trigger") and everyDefined(selectedRecords, "currentVersion.steps") and every(selectedRecords, "currentVersion.steps.length") and (everyEquals(selectedRecords, "currentVersion.status", "DRAFT") or includesNone(selectedRecords, "statuses", "ACTIVE")) and noneDefined(selectedRecords, "deletedAt")';