CommandMenuItem overridable entity (#21486)

## Context
Second PR of the overridable-entities track (after #21436 for views):
command menu items become overridable so that edits on non-owned items
are stored as overrides instead of mutating the row, and
deletion/deactivation becomes reversible.

 ## What this does

- `CommandMenuItemEntity` now extends
`OverridableEntity<CommandMenuItemOverrides>` (adds `isActive` +
`overrides`). All editable properties are overridable for now (to
discuss).
- **Update**: mutations on a command item not owned by the caller
(standard items) are written into `overrides`; reads merge them in the
DTO. The command palette edit mode (pin,
reorder, shortLabel) now preserves standard values, "Reset label to
default" gains true post-save semantics.
- **Delete**: protected items are deactivated (`isActive = false`)
instead of deleted; custom items still hard-delete.
- **Object deactivate/enable toggle**: now flips `isActive` on the
command item (merged into the main migration call) instead of
delete/recreate; a create-if-missing fallback covers legacy deactivated
objects.
- **Front**: inactive command items are filtered out of the palette
selector.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21486?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. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2026-06-13 14:21:27 +02:00
committed by GitHub
parent 036d9a2bcf
commit f63f053444
34 changed files with 481 additions and 78 deletions
@@ -138,7 +138,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isDefined(existingFlatObjectMetadata) &&
existingFlatObjectMetadata.isActive;
const { commandMenuItemsToCreate, commandMenuItemsToDelete } =
const { commandMenuItemsToCreate, commandMenuItemsToUpdate } =
this.computeCommandMenuItemChangesForActiveToggle({
isBeingEnabled,
isBeingDisabled,
@@ -175,6 +175,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToDelete: [],
flatEntityToUpdate: flatViewFieldsToUpdate,
},
commandMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: commandMenuItemsToUpdate,
},
},
workspaceId,
isSystemBuild: false,
@@ -190,18 +195,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
const hasCommandMenuItemChanges =
commandMenuItemsToCreate.length > 0 ||
commandMenuItemsToDelete.length > 0;
if (hasCommandMenuItemChanges) {
if (commandMenuItemsToCreate.length > 0) {
const commandMenuItemMigrationResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: commandMenuItemsToCreate,
flatEntityToDelete: commandMenuItemsToDelete,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
@@ -919,12 +920,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
applicationId: string;
}): {
commandMenuItemsToCreate: FlatCommandMenuItem[];
commandMenuItemsToDelete: FlatCommandMenuItem[];
commandMenuItemsToUpdate: FlatCommandMenuItem[];
} {
if (!isDefined(existingFlatObjectMetadata)) {
return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] };
return { commandMenuItemsToCreate: [], commandMenuItemsToUpdate: [] };
}
const now = new Date().toISOString();
if (isBeingEnabled) {
const existingCommandMenuItem =
this.findNavigationCommandMenuItemForObject({
@@ -943,28 +946,42 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatCommandMenuItemMaps,
}),
],
commandMenuItemsToDelete: [],
commandMenuItemsToUpdate: [],
};
}
if (!existingCommandMenuItem.isActive) {
return {
commandMenuItemsToCreate: [],
commandMenuItemsToUpdate: [
{ ...existingCommandMenuItem, isActive: true, updatedAt: now },
],
};
}
}
if (isBeingDisabled) {
const commandMenuItemToDelete =
const commandMenuItemToDeactivate =
this.findNavigationCommandMenuItemForObject({
objectUniversalIdentifier:
existingFlatObjectMetadata.universalIdentifier,
flatCommandMenuItemMaps,
});
if (isDefined(commandMenuItemToDelete)) {
if (
isDefined(commandMenuItemToDeactivate) &&
commandMenuItemToDeactivate.isActive
) {
return {
commandMenuItemsToCreate: [],
commandMenuItemsToDelete: [commandMenuItemToDelete],
commandMenuItemsToUpdate: [
{ ...commandMenuItemToDeactivate, isActive: false, updatedAt: now },
],
};
}
}
return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] };
return { commandMenuItemsToCreate: [], commandMenuItemsToUpdate: [] };
}
public async findOneWithinWorkspace(