From 4de9f45015e7c3259f84527ee6b37bb651811b7f Mon Sep 17 00:00:00 2001 From: Parship Chowdhury Date: Fri, 19 Jun 2026 16:51:50 +0530 Subject: [PATCH] fix: Edit Layout keeping the command menu open (#21161) ## Summary - Fixes https://github.com/twentyhq/core-team-issues/issues/2460 - Engine/headless commands always skipped closing the menu (`closeSidePanelOnCommandMenuListExecution: false`), even when the item was not pinned. Edit Layout is `isPinned: false`, so the menu should close like other list-only actions. ## Approach - Option 1 (I chose this one): Derive close behavior from `item.isPinned` -> pinned commands keep the menu open; non-pinned ones close it. - Option 2 (not chosen): remove the engine command override totally and use the default close behavior for all commands. Option 1 is more targeted: it fixed Edit Layout without changing pinned commands (e.g. Export progress in the menu list). Option 2 is simpler but widens the blast radius to every engine command clicked from the side panel list. ## Screenshots ### Before https://github.com/user-attachments/assets/70b8dc75-af00-4917-81a1-646381f571d5 ### After https://github.com/user-attachments/assets/f733d696-9330-4e6c-993b-8a8133c53e0d --------- Signed-off-by: Parship Chowdhury Co-authored-by: Charles Bochet --- ...ditRecordPageLayoutSingleRecordCommand.tsx | 10 +++-- .../side-panel/hooks/useSidePanelMenu.ts | 39 +++++++++++-------- .../side-panel/utils/waitForSidePanelClose.ts | 20 ++++++++++ 3 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 packages/twenty-front/src/modules/ui/layout/side-panel/utils/waitForSidePanelClose.ts diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx index 62183861fe..3fae4b6a9e 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx @@ -1,13 +1,17 @@ +import { useResetLocationHash } from 'twenty-ui/utilities'; + import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect'; import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; -import { useResetLocationHash } from 'twenty-ui/utilities'; +import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; export const EditRecordPageLayoutSingleRecordCommand = () => { const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode(); - + const { closeSidePanelMenu } = useSidePanelMenu(); const { resetLocationHash } = useResetLocationHash(); - const handleExecute = () => { + const handleExecute = async () => { + await closeSidePanelMenu(); + enterLayoutCustomizationMode(); resetLocationHash(); }; diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts index baf9b34a3c..cf414833bf 100644 --- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts +++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelMenu.ts @@ -11,6 +11,7 @@ import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelS import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState'; import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown'; import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent'; +import { waitForSidePanelClose } from '@/ui/layout/side-panel/utils/waitForSidePanelClose'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { t } from '@lingui/core/macro'; import { useStore } from 'jotai'; @@ -30,26 +31,30 @@ export const useSidePanelMenu = () => { const { removeFocusItemFromFocusStackById } = useRemoveFocusItemFromFocusStackById(); - const closeSidePanelMenu = useCallback(() => { + const closeSidePanelMenu = useCallback(async () => { const isSidePanelOpened = store.get(isSidePanelOpenedState.atom); - if (isSidePanelOpened) { - const isLayoutCustomizationModeEnabled = store.get( - isLayoutCustomizationModeEnabledState.atom, - ); - - if (isLayoutCustomizationModeEnabled) { - resetRecordIndexSelection(); - } - - store.set(sidePanelNavigationStackState.atom, []); - store.set(isSidePanelOpenedState.atom, false); - store.set(isSidePanelClosingState.atom, true); - closeAnyOpenDropdown(); - removeFocusItemFromFocusStackById({ - focusId: SIDE_PANEL_FOCUS_ID, - }); + if (!isSidePanelOpened) { + return; } + + const isLayoutCustomizationModeEnabled = store.get( + isLayoutCustomizationModeEnabledState.atom, + ); + + if (isLayoutCustomizationModeEnabled) { + resetRecordIndexSelection(); + } + + store.set(sidePanelNavigationStackState.atom, []); + store.set(isSidePanelOpenedState.atom, false); + store.set(isSidePanelClosingState.atom, true); + closeAnyOpenDropdown(); + removeFocusItemFromFocusStackById({ + focusId: SIDE_PANEL_FOCUS_ID, + }); + + await waitForSidePanelClose(); }, [ closeAnyOpenDropdown, removeFocusItemFromFocusStackById, diff --git a/packages/twenty-front/src/modules/ui/layout/side-panel/utils/waitForSidePanelClose.ts b/packages/twenty-front/src/modules/ui/layout/side-panel/utils/waitForSidePanelClose.ts new file mode 100644 index 0000000000..1ede934472 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/side-panel/utils/waitForSidePanelClose.ts @@ -0,0 +1,20 @@ +import { SIDE_PANEL_CLOSE_EVENT_NAME } from '@/ui/layout/side-panel/utils/emitSidePanelCloseEvent'; + +const SIDE_PANEL_CLOSE_TIMEOUT_MS = 500; + +export const waitForSidePanelClose = (): Promise => { + return new Promise((resolve) => { + const handler = () => { + window.removeEventListener(SIDE_PANEL_CLOSE_EVENT_NAME, handler); + clearTimeout(timeoutId); + resolve(); + }; + + const timeoutId = setTimeout(() => { + window.removeEventListener(SIDE_PANEL_CLOSE_EVENT_NAME, handler); + resolve(); + }, SIDE_PANEL_CLOSE_TIMEOUT_MS); + + window.addEventListener(SIDE_PANEL_CLOSE_EVENT_NAME, handler); + }); +};