From f0c3883fd1c23ba93f8ae30338dc0f50132e8e46 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 15 Jun 2026 18:10:42 +0200 Subject: [PATCH] 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 Review in cubic --- .../src/metadata/generated/schema.graphql | 1 + .../src/metadata/generated/schema.ts | 2 + .../src/metadata/generated/types.ts | 9 ++ .../src/generated-metadata/graphql.ts | 17 ++++ .../CommandMenuItemOptionsDropdown.tsx | 15 ++-- .../SidePanelCommandMenuItemEditPage.tsx | 18 ---- .../hooks/useResetCommandMenuItemToDefault.ts | 75 +++++++++++++++++ .../hooks/useResetCommandMenuItemsDraft.ts | 21 ----- .../hooks/useSaveCommandMenuItemsDraft.ts | 28 ++++++- .../graphql/mutations/resetCommandMenuItem.ts | 11 +++ .../command-menu-item.exception.ts | 3 + .../command-menu-item.resolver.ts | 9 ++ .../command-menu-item.service.ts | 84 +++++++++++++++++++ ...item-graphql-api-exception-handler.util.ts | 1 + 14 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault.ts delete mode 100644 packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft.ts create mode 100644 packages/twenty-front/src/modules/command-menu-item/graphql/mutations/resetCommandMenuItem.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 691054d149..69e5ca36a2 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -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! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 62adcd6131..d74f5db993 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -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} }) diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 9c5faf4403..5a8138972e 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -7585,6 +7585,15 @@ export default { ] } ], + "resetCommandMenuItem": [ + 35, + { + "id": [ + 3, + "UUID!" + ] + } + ], "deleteCommandMenuItem": [ 35, { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index b4fb17a2f2..d876fbeb76 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -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 | 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; 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; 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; +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; 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; 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; 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; diff --git a/packages/twenty-front/src/modules/command-menu-item/edit/components/CommandMenuItemOptionsDropdown.tsx b/packages/twenty-front/src/modules/command-menu-item/edit/components/CommandMenuItemOptionsDropdown.tsx index 39002fbbfa..61a0814571 100644 --- a/packages/twenty-front/src/modules/command-menu-item/edit/components/CommandMenuItemOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/edit/components/CommandMenuItemOptionsDropdown.tsx @@ -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 = ({ /> diff --git a/packages/twenty-front/src/modules/command-menu-item/edit/components/SidePanelCommandMenuItemEditPage.tsx b/packages/twenty-front/src/modules/command-menu-item/edit/components/SidePanelCommandMenuItemEditPage.tsx index a091b6150e..5f733c6c25 100644 --- a/packages/twenty-front/src/modules/command-menu-item/edit/components/SidePanelCommandMenuItemEditPage.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/edit/components/SidePanelCommandMenuItemEditPage.tsx @@ -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 = () => { - , - ]} - /> ); }; diff --git a/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault.ts b/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault.ts new file mode 100644 index 0000000000..1e52ef86d8 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft.ts b/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft.ts deleted file mode 100644 index f92da8b35f..0000000000 --- a/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft.ts +++ /dev/null @@ -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 }; -}; diff --git a/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft.ts b/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft.ts index 107642c230..857585366e 100644 --- a/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft.ts +++ b/packages/twenty-front/src/modules/command-menu-item/edit/hooks/useSaveCommandMenuItemsDraft.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/command-menu-item/graphql/mutations/resetCommandMenuItem.ts b/packages/twenty-front/src/modules/command-menu-item/graphql/mutations/resetCommandMenuItem.ts new file mode 100644 index 0000000000..d47e785f8f --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu-item/graphql/mutations/resetCommandMenuItem.ts @@ -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 + } + } +`; diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.exception.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.exception.ts index c5b833f60a..1a74d7b383 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.exception.ts @@ -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); } diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts index 4d39e9a9bc..034561f7db 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts @@ -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 { + return await this.commandMenuItemService.reset(id, workspace.id); + } + @Mutation(() => CommandMenuItemDTO) @UseGuards(NoPermissionGuard) async deleteCommandMenuItem( diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts index 283401f7da..01ab54e29a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.service.ts @@ -245,6 +245,90 @@ export class CommandMenuItemService { ); } + async reset(id: string, workspaceId: string): Promise { + 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 { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/command-menu-item-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/command-menu-item-graphql-api-exception-handler.util.ts index 6e6b55c458..2c215c6fd2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/command-menu-item-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/command-menu-item-graphql-api-exception-handler.util.ts @@ -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);