Replace hotkey scopes by focus stack (Part 6 - Remove Hotkey scopes 🫳🎤) (#13127)

# Replace hotkey scopes by focus stack (Part 6 - Remove Hotkey scopes)

This PR is the last part of a refactoring aiming to deprecate the hotkey
scopes api in favor of the new focus stack api which is more robust.
Part 1: https://github.com/twentyhq/twenty/pull/12673
Part 2: https://github.com/twentyhq/twenty/pull/12798
Part 3: https://github.com/twentyhq/twenty/pull/12910
Part 4: https://github.com/twentyhq/twenty/pull/12933
Part 5: https://github.com/twentyhq/twenty/pull/13106

In this part, we completely remove the hotkey scopes.
This commit is contained in:
Raphaël Bosi
2025-07-09 17:21:14 +02:00
committed by GitHub
parent 0a7b21234b
commit eba997be98
215 changed files with 687 additions and 1424 deletions

View File

@ -7,7 +7,6 @@ import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/Comman
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
@ -78,7 +77,6 @@ export const CommandMenuList = ({
selectableListInstanceId="command-menu-list"
focusId={SIDE_PANEL_FOCUS_ID}
selectableItemIdArray={selectableItemIds}
hotkeyScope={AppHotkeyScope.CommandMenuOpen}
onSelect={() => {
setHasUserSelectedCommand(true);
}}

View File

@ -1,13 +1,13 @@
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/constants/RecordChipClickOutsideId';
import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramCreateStepNodeClickOutsideId';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
@ -55,11 +55,11 @@ export const CommandMenuOpenContainer = ({
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent | TouchEvent) => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
const currentFocusId = snapshot
.getLoadable(currentFocusIdSelector)
.getValue();
if (hotkeyScope?.scope === CommandMenuHotkeyScope.CommandMenuFocused) {
if (currentFocusId === SIDE_PANEL_FOCUS_ID) {
event.stopImmediatePropagation();
event.preventDefault();
closeCommandMenu();

View File

@ -8,18 +8,6 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
const mockGoBackToPreviousHotkeyScope = jest.fn();
const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn();
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({
usePreviousHotkeyScope: () => ({
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope:
mockSetHotkeyScopeAndMemorizePreviousScope,
}),
}));
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
@ -71,13 +59,6 @@ describe('useCommandMenu', () => {
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
scope: CommandMenuHotkeyScope.CommandMenuFocused,
memoizeKey: 'command-menu',
customScopes: {
commandMenuOpen: true,
},
});
act(() => {
result.current.commandMenu.closeCommandMenu();
@ -96,13 +77,6 @@ describe('useCommandMenu', () => {
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({
scope: CommandMenuHotkeyScope.CommandMenuFocused,
memoizeKey: 'command-menu',
customScopes: {
commandMenuOpen: true,
},
});
act(() => {
result.current.commandMenu.toggleCommandMenu();
@ -110,21 +84,4 @@ describe('useCommandMenu', () => {
expect(result.current.isCommandMenuOpened).toBe(false);
});
it('should call goBackToPreviousHotkeyScope when closing the command menu', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.openCommandMenu();
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockGoBackToPreviousHotkeyScope).not.toHaveBeenCalled();
act(() => {
result.current.commandMenu.closeCommandMenu();
});
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalledTimes(1);
});
});

View File

@ -6,15 +6,12 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { isDefined } from 'twenty-shared/utils';
export const useCommandMenuHistory = () => {
const { closeCommandMenu } = useCommandMenu();
const setHotkeyScope = useSetHotkeyScope();
const goBackFromCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
@ -69,78 +66,64 @@ export const useCommandMenuHistory = () => {
}
set(hasUserSelectedCommandState, false);
setHotkeyScope(CommandMenuHotkeyScope.CommandMenuFocused, {
commandMenuOpen: true,
});
};
},
[closeCommandMenu, setHotkeyScope],
[closeCommandMenu],
);
const navigateCommandMenuHistory = useRecoilCallback(
({ snapshot, set }) => {
return (pageIndex: number) => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const navigateCommandMenuHistory = useRecoilCallback(({ snapshot, set }) => {
return (pageIndex: number) => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const newNavigationStack = currentNavigationStack.slice(
0,
pageIndex + 1,
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
set(commandMenuNavigationStackState, newNavigationStack);
const newNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(newNavigationStackItem)) {
throw new Error(
`No command menu navigation stack item found for index ${pageIndex}`,
);
}
set(commandMenuNavigationStackState, newNavigationStack);
set(commandMenuPageState, newNavigationStackItem.page);
set(commandMenuPageInfoState, {
title: newNavigationStackItem.pageTitle,
Icon: newNavigationStackItem.pageIcon,
instanceId: newNavigationStackItem.pageId,
});
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const newNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(newNavigationStackItem)) {
throw new Error(
`No command menu navigation stack item found for index ${pageIndex}`,
for (const [pageId, morphItem] of currentMorphItems.entries()) {
if (!newNavigationStack.some((item) => item.pageId === pageId)) {
set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId,
targetObjectId: morphItem.recordId,
}),
}),
null,
);
}
}
set(commandMenuPageState, newNavigationStackItem.page);
set(commandMenuPageInfoState, {
title: newNavigationStackItem.pageTitle,
Icon: newNavigationStackItem.pageIcon,
instanceId: newNavigationStackItem.pageId,
});
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId),
),
);
for (const [pageId, morphItem] of currentMorphItems.entries()) {
if (!newNavigationStack.some((item) => item.pageId === pageId)) {
set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId,
targetObjectId: morphItem.recordId,
}),
}),
null,
);
}
}
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId),
),
);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
set(hasUserSelectedCommandState, false);
setHotkeyScope(CommandMenuHotkeyScope.CommandMenuFocused, {
commandMenuOpen: true,
});
};
},
[setHotkeyScope],
);
set(hasUserSelectedCommandState, false);
};
}, []);
return {
goBackFromCommandMenu,

View File

@ -1,16 +1,16 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
@ -43,7 +43,6 @@ export const useCommandMenuHotKeys = () => {
toggleCommandMenu();
},
true,
AppHotkeyScope.CommandMenu,
[closeKeyboardShortcutMenu, toggleCommandMenu],
);
@ -53,26 +52,24 @@ export const useCommandMenuHotKeys = () => {
openRecordsSearchPage();
},
false,
AppHotkeyScope.SearchRecords,
[openRecordsSearchPage],
{
ignoreModifiers: true,
},
);
useGlobalHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
goBackFromCommandMenu();
},
true,
CommandMenuHotkeyScope.CommandMenuFocused,
[goBackFromCommandMenu],
);
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [goBackFromCommandMenu],
});
useGlobalHotkeys(
[Key.Backspace, Key.Delete],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Backspace, Key.Delete],
callback: () => {
if (isNonEmptyString(commandMenuSearch)) {
return;
}
@ -91,17 +88,13 @@ export const useCommandMenuHotKeys = () => {
goBackFromCommandMenu();
}
},
true,
CommandMenuHotkeyScope.CommandMenuFocused,
[
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [
commandMenuPage,
commandMenuSearch,
contextStoreTargetedRecordsRuleComponent,
goBackFromCommandMenu,
setGlobalCommandMenuContext,
],
{
preventDefault: false,
},
);
});
};

View File

@ -10,7 +10,6 @@ import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
@ -61,13 +60,9 @@ export const useNavigateCommandMenu = () => {
type: FocusComponentType.SIDE_PANEL,
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
},
hotkeyScope: {
scope: CommandMenuHotkeyScope.CommandMenuFocused,
customScopes: {
commandMenuOpen: true,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: COMMAND_MENU_COMPONENT_INSTANCE_ID,
});
copyContextStoreStates({

View File

@ -1,3 +0,0 @@
export enum CommandMenuHotkeyScope {
CommandMenuFocused = 'command-menu-focused',
}