fix(command-menu-item): persist overrides after save and add reset-to-default (#21623)

## Context
Command menu items are an overridable entity (like page-layout / FIELDS
widgets), but the override flow in layout-customization mode was broken:

- **Move / pin-unpin / hide-label didn't persist.**
`useSaveCommandMenuItemsDraft` fired the `updateCommandMenuItem`
mutations (backend persisted correctly) but never wrote the result back
into `metadataStoreState`, the source the live menu and edit panel read
from. So the UI reverted on exit and changes only showed after a hard
reload.
- **No true "reset to default".** Existing reset controls only reverted
the draft to the last-saved values (which still contained overrides).
there was no way to clear overrides back to the original values after a
save.

Notes
- removed the footer "Reset to default" button. This is not clear to me
how we want to build, let's re-implement better in the next version
- reset are done on click and not delayed on the save. This is similar
to other reset to default on page layouts where we actually usually
reload the component and this is because the FE has no idea what's
original VS override from the response itself

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21623?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:
Weiko
2026-06-15 18:10:42 +02:00
committed by GitHub
parent 9dd097e11e
commit f0c3883fd1
14 changed files with 243 additions and 51 deletions
@@ -8,6 +8,7 @@ export enum CommandMenuItemExceptionCode {
COMMAND_MENU_ITEM_NOT_FOUND = 'COMMAND_MENU_ITEM_NOT_FOUND',
INVALID_COMMAND_MENU_ITEM_INPUT = 'INVALID_COMMAND_MENU_ITEM_INPUT',
WORKFLOW_OR_FRONT_COMPONENT_REQUIRED = 'WORKFLOW_OR_FRONT_COMPONENT_REQUIRED',
COMMAND_MENU_ITEM_CANNOT_BE_RESET = 'COMMAND_MENU_ITEM_CANNOT_BE_RESET',
}
const getCommandMenuItemExceptionUserFriendlyMessage = (
@@ -20,6 +21,8 @@ const getCommandMenuItemExceptionUserFriendlyMessage = (
return msg`Invalid command menu item input.`;
case CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED:
return msg`Either workflow version or front component is required.`;
case CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_CANNOT_BE_RESET:
return msg`Custom command menu item cannot be reset to default.`;
default:
assertUnreachable(code);
}
@@ -136,6 +136,15 @@ export class CommandMenuItemResolver {
return await this.commandMenuItemService.update(input, workspace.id);
}
@Mutation(() => CommandMenuItemDTO)
@UseGuards(NoPermissionGuard)
async resetCommandMenuItem(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO> {
return await this.commandMenuItemService.reset(id, workspace.id);
}
@Mutation(() => CommandMenuItemDTO)
@UseGuards(NoPermissionGuard)
async deleteCommandMenuItem(
@@ -245,6 +245,90 @@ export class CommandMenuItemService {
);
}
async reset(id: string, workspaceId: string): Promise<CommandMenuItemDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
const existingFlatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: existingFlatCommandMenuItemMaps,
});
if (!isDefined(existingFlatCommandMenuItem)) {
throw new CommandMenuItemException(
'Command menu item not found',
CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
);
}
if (
existingFlatCommandMenuItem.applicationUniversalIdentifier ===
workspaceCustomFlatApplication.universalIdentifier
) {
throw new CommandMenuItemException(
'Custom command menu item cannot be reset to default',
CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_CANNOT_BE_RESET,
);
}
const flatCommandMenuItemToUpdate: FlatCommandMenuItem = {
...existingFlatCommandMenuItem,
isActive: true,
overrides: null,
universalOverrides: null,
updatedAt: new Date().toISOString(),
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatCommandMenuItemToUpdate],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while resetting command menu item to default',
);
}
const { flatCommandMenuItemMaps: recomputedFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
return fromFlatCommandMenuItemToCommandMenuItemDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatCommandMenuItemMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<CommandMenuItemDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
@@ -16,6 +16,7 @@ export const commandMenuItemGraphqlApiExceptionHandler = (error: Error) => {
throw new NotFoundError(error);
case CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT:
case CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED:
case CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_CANNOT_BE_RESET:
throw new UserInputError(error);
default: {
return assertUnreachable(error.code);