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
@@ -3287,6 +3287,7 @@ type Mutation {
updateOneLogicFunction(input: UpdateLogicFunctionFromSourceInput!): Boolean!
createCommandMenuItem(input: CreateCommandMenuItemInput!): CommandMenuItem!
updateCommandMenuItem(input: UpdateCommandMenuItemInput!): CommandMenuItem!
resetCommandMenuItem(id: UUID!): CommandMenuItem!
deleteCommandMenuItem(id: UUID!): CommandMenuItem!
createFrontComponent(input: CreateFrontComponentInput!): FrontComponent!
updateFrontComponent(input: UpdateFrontComponentInput!): FrontComponent!
@@ -2809,6 +2809,7 @@ export interface Mutation {
updateOneLogicFunction: Scalars['Boolean']
createCommandMenuItem: CommandMenuItem
updateCommandMenuItem: CommandMenuItem
resetCommandMenuItem: CommandMenuItem
deleteCommandMenuItem: CommandMenuItem
createFrontComponent: FrontComponent
updateFrontComponent: FrontComponent
@@ -5952,6 +5953,7 @@ export interface MutationGenqlSelection{
updateOneLogicFunction?: { __args: {input: UpdateLogicFunctionFromSourceInput} }
createCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: CreateCommandMenuItemInput} })
updateCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {input: UpdateCommandMenuItemInput} })
resetCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
deleteCommandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
createFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: CreateFrontComponentInput} })
updateFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: UpdateFrontComponentInput} })
@@ -7585,6 +7585,15 @@ export default {
]
}
],
"resetCommandMenuItem": [
35,
{
"id": [
3,
"UUID!"
]
}
],
"deleteCommandMenuItem": [
35,
{
@@ -2549,6 +2549,7 @@ export type Mutation = {
renewToken: AuthTokens;
resendEmailVerificationToken: ResendEmailVerificationToken;
resendWorkspaceInvitation: SendInvitations;
resetCommandMenuItem: CommandMenuItem;
resetPageLayoutTabToDefault: PageLayoutTab;
resetPageLayoutToDefault: PageLayout;
resetPageLayoutWidgetToDefault: PageLayoutWidget;
@@ -3240,6 +3241,11 @@ export type MutationResendWorkspaceInvitationArgs = {
};
export type MutationResetCommandMenuItemArgs = {
id: Scalars['UUID']['input'];
};
export type MutationResetPageLayoutTabToDefaultArgs = {
id: Scalars['String']['input'];
};
@@ -6750,6 +6756,16 @@ export type CommandMenuItemFieldsFragment = { __typename?: 'CommandMenuItem', id
| { __typename?: 'PathCommandMenuItemPayload', path: string }
| null };
export type ResetCommandMenuItemMutationVariables = Exact<{
id: Scalars['UUID']['input'];
}>;
export type ResetCommandMenuItemMutation = { __typename?: 'Mutation', resetCommandMenuItem: { __typename?: 'CommandMenuItem', id: string, applicationId?: string | null, workflowVersionId?: string | null, frontComponentId?: string | null, engineComponentKey: EngineComponentKey, label: string, icon?: string | null, shortLabel?: string | null, position: number, isPinned: boolean, hotKeys?: Array<string> | null, conditionalAvailabilityExpression?: string | null, availabilityType: CommandMenuItemAvailabilityType, availabilityObjectMetadataId?: string | null, pageLayoutId?: string | null, isActive: boolean, frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, isHeadless: boolean } | null, payload?:
| { __typename?: 'ObjectMetadataCommandMenuItemPayload', objectMetadataItemId: string }
| { __typename?: 'PathCommandMenuItemPayload', path: string }
| null } };
export type UpdateCommandMenuItemMutationVariables = Exact<{
input: UpdateCommandMenuItemInput;
}>;
@@ -8616,6 +8632,7 @@ export const CheckUserExistsDocument = {"kind":"Document","definitions":[{"kind"
export const FindApplicationRegistrationByClientIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindApplicationRegistrationByClientId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"clientId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findApplicationRegistrationByClientId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"clientId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"clientId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}}]}}]}}]} as unknown as DocumentNode<FindApplicationRegistrationByClientIdQuery, FindApplicationRegistrationByClientIdQueryVariables>;
export const GetPublicWorkspaceDataByDomainDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPublicWorkspaceDataByDomain"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"origin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getPublicWorkspaceDataByDomain"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"origin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"origin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceUrlsFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"}}]}},{"kind":"Field","name":{"kind":"Name","value":"google"}},{"kind":"Field","name":{"kind":"Name","value":"magicLink"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"microsoft"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authBypassProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"google"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"microsoft"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceUrlsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUrls"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}},{"kind":"Field","name":{"kind":"Name","value":"customUrl"}}]}}]} as unknown as DocumentNode<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>;
export const ValidatePasswordResetTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidatePasswordResetToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validatePasswordResetToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"passwordResetToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"hasPassword"}}]}}]}}]} as unknown as DocumentNode<ValidatePasswordResetTokenQuery, ValidatePasswordResetTokenQueryVariables>;
export const ResetCommandMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ResetCommandMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resetCommandMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommandMenuItemFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommandMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CommandMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"workflowVersionId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponentId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}}]}},{"kind":"Field","name":{"kind":"Name","value":"engineComponentKey"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"shortLabel"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"isPinned"}},{"kind":"Field","name":{"kind":"Name","value":"payload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PathCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectMetadataCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectMetadataItemId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"hotKeys"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalAvailabilityExpression"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityType"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}}]} as unknown as DocumentNode<ResetCommandMenuItemMutation, ResetCommandMenuItemMutationVariables>;
export const UpdateCommandMenuItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCommandMenuItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCommandMenuItemInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCommandMenuItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommandMenuItemFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommandMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CommandMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"workflowVersionId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponentId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}}]}},{"kind":"Field","name":{"kind":"Name","value":"engineComponentKey"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"shortLabel"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"isPinned"}},{"kind":"Field","name":{"kind":"Name","value":"payload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PathCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectMetadataCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectMetadataItemId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"hotKeys"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalAvailabilityExpression"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityType"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}}]} as unknown as DocumentNode<UpdateCommandMenuItemMutation, UpdateCommandMenuItemMutationVariables>;
export const FindManyCommandMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyCommandMenuItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commandMenuItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommandMenuItemFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommandMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CommandMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"workflowVersionId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponentId"}},{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}}]}},{"kind":"Field","name":{"kind":"Name","value":"engineComponentKey"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"shortLabel"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"isPinned"}},{"kind":"Field","name":{"kind":"Name","value":"payload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PathCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectMetadataCommandMenuItemPayload"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectMetadataItemId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"hotKeys"}},{"kind":"Field","name":{"kind":"Name","value":"conditionalAvailabilityExpression"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityType"}},{"kind":"Field","name":{"kind":"Name","value":"availabilityObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}}]} as unknown as DocumentNode<FindManyCommandMenuItemsQuery, FindManyCommandMenuItemsQueryVariables>;
export const DuplicateDashboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DuplicateDashboard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duplicateDashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<DuplicateDashboardMutation, DuplicateDashboardMutationVariables>;
@@ -1,3 +1,4 @@
import { useResetCommandMenuItemToDefault } from '@/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault';
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/edit/hooks/useUpdateCommandMenuItemInDraft';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -34,14 +35,13 @@ export const CommandMenuItemOptionsDropdown = ({
const dropdownId = getCommandMenuItemOptionsDropdownId(itemId);
const { closeDropdown } = useCloseDropdown();
const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
const { resetCommandMenuItemToDefault } = useResetCommandMenuItemToDefault();
const normalizedServerShortLabel = serverShortLabel ?? null;
const normalizedShortLabel = shortLabel ?? null;
const hasNoShortLabel = normalizedServerShortLabel === null;
const isLabelHidden =
normalizedShortLabel === null && isDefined(normalizedServerShortLabel);
const hasShortLabelOverride =
normalizedShortLabel !== normalizedServerShortLabel;
const handleToggleHideLabel = (toggled: boolean) => {
updateCommandMenuItemInDraft(itemId, {
@@ -49,11 +49,9 @@ export const CommandMenuItemOptionsDropdown = ({
});
};
const handleResetLabelToDefault = () => {
updateCommandMenuItemInDraft(itemId, {
shortLabel: normalizedServerShortLabel,
});
const handleResetToDefault = async () => {
closeDropdown(dropdownId);
await resetCommandMenuItemToDefault(itemId);
};
return (
@@ -74,10 +72,9 @@ export const CommandMenuItemOptionsDropdown = ({
/>
<MenuItem
LeftIcon={IconRefresh}
onClick={handleResetLabelToDefault}
onClick={handleResetToDefault}
accent="default"
text={t`Reset label to default`}
disabled={!hasShortLabelOverride}
text={t`Reset to default`}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
@@ -2,7 +2,6 @@ import { CommandMenuItemEditRecordSelectionDropdown } from '@/command-menu-item/
import { CommandMenuItemOptionsDropdown } from '@/command-menu-item/edit/components/CommandMenuItemOptionsDropdown';
import { useEditableCommandMenuItems } from '@/command-menu-item/edit/hooks/useEditableCommandMenuItems';
import { useReorderCommandMenuItemsInDraft } from '@/command-menu-item/edit/hooks/useReorderCommandMenuItemsInDraft';
import { useResetCommandMenuItemsDraft } from '@/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft';
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/edit/hooks/useUpdateCommandMenuItemInDraft';
import { useCurrentCommandMenuContextApi } from '@/command-menu-item/hooks/useCurrentCommandMenuContextApi';
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
@@ -14,7 +13,6 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type DropResult } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
@@ -28,10 +26,8 @@ import {
IconDotsVertical,
IconPin,
IconPinnedOff,
IconRefresh,
useIcons,
} from 'twenty-ui-deprecated/display';
import { Button } from 'twenty-ui-deprecated/input';
import { MenuItem, MenuItemDraggable } from 'twenty-ui-deprecated/navigation';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
@@ -79,7 +75,6 @@ export const SidePanelCommandMenuItemEditPage = () => {
);
const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
const { reorderCommandMenuItemInDraft } = useReorderCommandMenuItemsInDraft();
const { resetCommandMenuItemsDraft } = useResetCommandMenuItemsDraft();
const editableCommandMenuItems = useEditableCommandMenuItems();
@@ -298,19 +293,6 @@ export const SidePanelCommandMenuItemEditPage = () => {
</SidePanelGroup>
</SidePanelList>
</StyledContent>
<SidePanelFooter
actions={[
<Button
key="reset"
Icon={IconRefresh}
title={t`Reset to default`}
variant="secondary"
accent="default"
size="small"
onClick={resetCommandMenuItemsDraft}
/>,
]}
/>
</StyledContainer>
);
};
@@ -0,0 +1,75 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CrudOperationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
import { RESET_COMMAND_MENU_ITEM } from '@/command-menu-item/graphql/mutations/resetCommandMenuItem';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
type ResetCommandMenuItemMutation,
type ResetCommandMenuItemMutationVariables,
} from '~/generated-metadata/graphql';
export const useResetCommandMenuItemToDefault = () => {
const store = useStore();
const [resetCommandMenuItem] = useMutation<
ResetCommandMenuItemMutation,
ResetCommandMenuItemMutationVariables
>(RESET_COMMAND_MENU_ITEM);
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const resetCommandMenuItemToDefault = useCallback(
async (id: string) => {
try {
const result = await resetCommandMenuItem({ variables: { id } });
const resetItem = result.data?.resetCommandMenuItem;
if (!isDefined(resetItem)) {
return;
}
updateInDraft('commandMenuItems', [resetItem]);
applyChanges();
const draft = store.get(commandMenuItemsDraftState.atom);
if (isDefined(draft)) {
store.set(
commandMenuItemsDraftState.atom,
draft.map((item) =>
item.id === id ? { ...item, ...resetItem } : item,
),
);
}
} catch (error) {
if (CombinedGraphQLErrors.is(error)) {
handleMetadataError(error, {
primaryMetadataName: 'commandMenuItem',
operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
}
},
[
resetCommandMenuItem,
updateInDraft,
applyChanges,
store,
handleMetadataError,
enqueueErrorSnackBar,
],
);
return { resetCommandMenuItemToDefault };
};
@@ -1,21 +0,0 @@
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
// Resets the draft to the current server state, discarding all user edits.
export const useResetCommandMenuItemsDraft = () => {
const store = useStore();
const resetCommandMenuItemsDraft = useCallback(() => {
const serverItems = store.get(commandMenuItemsSelector.atom);
store.set(
commandMenuItemsDraftState.atom,
serverItems.map((item) => ({ ...item })),
);
}, [store]);
return { resetCommandMenuItemsDraft };
};
@@ -6,13 +6,22 @@ import { isDefined } from 'twenty-shared/utils';
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
import { UPDATE_COMMAND_MENU_ITEM } from '@/command-menu-item/graphql/mutations/updateCommandMenuItem';
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type UpdateCommandMenuItemInput } from '~/generated-metadata/graphql';
import {
type UpdateCommandMenuItemInput,
type UpdateCommandMenuItemMutation,
type UpdateCommandMenuItemMutationVariables,
} from '~/generated-metadata/graphql';
export const useSaveCommandMenuItemsDraft = () => {
const store = useStore();
const [updateCommandMenuItem] = useMutation(UPDATE_COMMAND_MENU_ITEM);
const [updateCommandMenuItem] = useMutation<
UpdateCommandMenuItemMutation,
UpdateCommandMenuItemMutationVariables
>(UPDATE_COMMAND_MENU_ITEM);
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
const saveCommandMenuItemsDraft = useCallback(async () => {
const draft = store.get(commandMenuItemsDraftState.atom);
@@ -39,6 +48,10 @@ export const useSaveCommandMenuItemsDraft = () => {
);
});
if (changedItems.length === 0) {
return;
}
await Promise.all(
changedItems.map((item) => {
const input: UpdateCommandMenuItemInput = {
@@ -51,7 +64,16 @@ export const useSaveCommandMenuItemsDraft = () => {
return updateCommandMenuItem({ variables: { input } });
}),
);
}, [store, commandMenuItems, updateCommandMenuItem]);
updateInDraft('commandMenuItems', changedItems);
applyChanges();
}, [
store,
commandMenuItems,
updateCommandMenuItem,
updateInDraft,
applyChanges,
]);
return { saveCommandMenuItemsDraft };
};
@@ -0,0 +1,11 @@
import { COMMAND_MENU_ITEM_FRAGMENT } from '@/command-menu-item/graphql/fragments/commandMenuItemFragment';
import { gql } from '@apollo/client';
export const RESET_COMMAND_MENU_ITEM = gql`
${COMMAND_MENU_ITEM_FRAGMENT}
mutation ResetCommandMenuItem($id: UUID!) {
resetCommandMenuItem(id: $id) {
...CommandMenuItemFields
}
}
`;
@@ -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);