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
@@ -38,7 +38,9 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
configuration: 'configuration',
overrides: 'overrides',
},
commandMenuItem: {},
commandMenuItem: {
overrides: 'overrides',
},
navigationMenuItem: {},
rolePermissionFlag: {},
permissionFlag: {},
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
@@ -10,6 +11,7 @@ import {
FlatCreateCommandMenuItemAction,
UniversalCreateCommandMenuItemAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import { fromUniversalOverridesToCommandMenuItemOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/utils/from-universal-overrides-to-command-menu-item-overrides.util';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
@@ -37,6 +39,14 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio
universalForeignKeyValues: action.flatEntity,
});
const overrides = isDefined(action.flatEntity.universalOverrides)
? fromUniversalOverridesToCommandMenuItemOverrides({
universalOverrides: action.flatEntity.universalOverrides,
flatObjectMetadataMaps: allFlatEntityMaps.flatObjectMetadataMaps,
flatPageLayoutMaps: allFlatEntityMaps.flatPageLayoutMaps,
})
: null;
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'commandMenuItem',
@@ -49,6 +59,7 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio
availabilityObjectMetadataId,
frontComponentId,
pageLayoutId,
overrides,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
@@ -7,6 +7,7 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import { fromUniversalOverridesToCommandMenuItemOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/utils/from-universal-overrides-to-command-menu-item-overrides.util';
import {
FlatUpdateCommandMenuItemAction,
UniversalUpdateCommandMenuItemAction,
@@ -31,11 +32,27 @@ export class UpdateCommandMenuItemActionHandlerService extends WorkspaceMigratio
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'commandMenuItem',
universalUpdate: action.update,
allFlatEntityMaps,
});
const { universalOverrides, ...updateWithResolvedForeignKeys } =
resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'commandMenuItem',
universalUpdate: action.update,
allFlatEntityMaps,
});
const update =
universalOverrides === undefined
? updateWithResolvedForeignKeys
: universalOverrides === null
? { ...updateWithResolvedForeignKeys, overrides: null }
: {
...updateWithResolvedForeignKeys,
overrides: fromUniversalOverridesToCommandMenuItemOverrides({
universalOverrides,
flatObjectMetadataMaps:
allFlatEntityMaps.flatObjectMetadataMaps,
flatPageLayoutMaps: allFlatEntityMaps.flatPageLayoutMaps,
}),
};
return {
type: 'update',
@@ -0,0 +1,58 @@
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
import { type CommandMenuItemOverrides } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
type UniversalCommandMenuItemOverrides =
FormatRecordSerializedRelationProperties<CommandMenuItemOverrides>;
export const fromUniversalOverridesToCommandMenuItemOverrides = ({
universalOverrides,
flatObjectMetadataMaps,
flatPageLayoutMaps,
}: {
universalOverrides: UniversalCommandMenuItemOverrides;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatPageLayoutMaps: FlatEntityMaps<FlatPageLayout>;
}): CommandMenuItemOverrides => {
const {
availabilityObjectMetadataUniversalIdentifier,
pageLayoutUniversalIdentifier,
...scalarOverrides
} = universalOverrides;
const overrides: CommandMenuItemOverrides = { ...scalarOverrides };
if (availabilityObjectMetadataUniversalIdentifier !== undefined) {
if (availabilityObjectMetadataUniversalIdentifier === null) {
overrides.availabilityObjectMetadataId = null;
} else {
const flatObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: availabilityObjectMetadataUniversalIdentifier,
});
overrides.availabilityObjectMetadataId = flatObjectMetadata?.id ?? null;
}
}
if (pageLayoutUniversalIdentifier !== undefined) {
if (pageLayoutUniversalIdentifier === null) {
overrides.pageLayoutId = null;
} else {
const flatPageLayout =
findFlatEntityByUniversalIdentifier<FlatPageLayout>({
flatEntityMaps: flatPageLayoutMaps,
universalIdentifier: pageLayoutUniversalIdentifier,
});
overrides.pageLayoutId = flatPageLayout?.id ?? null;
}
}
return overrides;
};