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

@ -0,0 +1,324 @@
import { DEFAULT_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig';
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKeys';
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionType } from '@/action-menu/actions/types/ActionType';
import { DefaultRecordActionConfigKeys } from '@/action-menu/actions/types/DefaultRecordActionConfigKeys';
import { IconHeart, IconPlus } from 'twenty-ui/display';
import { inheritActionsFromDefaultConfig } from '../inheritActionsFromDefaultConfig';
const MockComponent = <div>Mock Component</div>;
describe('inheritActionsFromDefaultConfig', () => {
it('should return empty object when no action keys are provided', () => {
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys: [],
propertiesToOverwrite: {},
});
expect(result).toEqual({});
});
it('should return only provided config when no default action keys are specified', () => {
const customConfig: Record<string, ActionConfig> = {
'custom-action': {
type: ActionType.Standard,
scope: ActionScope.Object,
key: 'custom-action',
label: 'Custom Action',
position: 100,
Icon: IconPlus,
shouldBeRegistered: () => true,
component: MockComponent,
},
};
const result = inheritActionsFromDefaultConfig({
config: customConfig,
actionKeys: [],
propertiesToOverwrite: {},
});
expect(result).toEqual(customConfig);
});
it('should inherit actions from default config', () => {
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
SingleRecordActionKeys.ADD_TO_FAVORITES,
];
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys,
propertiesToOverwrite: {},
});
expect(result).toEqual({
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]:
DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
[SingleRecordActionKeys.ADD_TO_FAVORITES]:
DEFAULT_RECORD_ACTIONS_CONFIG[SingleRecordActionKeys.ADD_TO_FAVORITES],
});
});
it('should overwrite specific properties of inherited actions', () => {
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
];
const propertiesToOverwrite = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {
label: 'Custom Create Label',
position: 999,
isPinned: false,
},
};
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys,
propertiesToOverwrite,
});
const expectedAction = {
...DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
label: 'Custom Create Label',
position: 999,
isPinned: false,
};
expect(result).toEqual({
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: expectedAction,
});
});
it('should overwrite properties for multiple actions', () => {
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
SingleRecordActionKeys.ADD_TO_FAVORITES,
];
const propertiesToOverwrite = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {
position: 10,
},
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
label: 'Custom Favorite Label',
Icon: IconHeart,
},
};
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys,
propertiesToOverwrite,
});
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]).toEqual({
...DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
position: 10,
});
expect(result[SingleRecordActionKeys.ADD_TO_FAVORITES]).toEqual({
...DEFAULT_RECORD_ACTIONS_CONFIG[SingleRecordActionKeys.ADD_TO_FAVORITES],
label: 'Custom Favorite Label',
Icon: IconHeart,
});
});
it('should only overwrite properties for specified actions', () => {
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
SingleRecordActionKeys.ADD_TO_FAVORITES,
];
const propertiesToOverwrite = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {
position: 10,
},
};
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys,
propertiesToOverwrite,
});
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]).toEqual({
...DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
position: 10,
});
expect(result[SingleRecordActionKeys.ADD_TO_FAVORITES]).toEqual(
DEFAULT_RECORD_ACTIONS_CONFIG[SingleRecordActionKeys.ADD_TO_FAVORITES],
);
});
it('should merge inherited actions with provided config', () => {
const customConfig: Record<string, ActionConfig> = {
'custom-action': {
type: ActionType.Standard,
scope: ActionScope.Object,
key: 'custom-action',
label: 'Custom Action',
position: 100,
Icon: IconPlus,
shouldBeRegistered: () => true,
component: MockComponent,
},
};
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
];
const result = inheritActionsFromDefaultConfig({
config: customConfig,
actionKeys,
propertiesToOverwrite: {},
});
expect(result).toEqual({
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]:
DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
'custom-action': customConfig['custom-action'],
});
});
it('should prioritize provided config over inherited actions when keys conflict', () => {
const customConfig: Record<string, ActionConfig> = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {
type: ActionType.Standard,
scope: ActionScope.Object,
key: NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
label: 'Overridden Create Action',
position: 999,
Icon: IconHeart,
shouldBeRegistered: () => false,
component: MockComponent,
},
};
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
];
const result = inheritActionsFromDefaultConfig({
config: customConfig,
actionKeys,
propertiesToOverwrite: {},
});
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]).toEqual(
customConfig[NoSelectionRecordActionKeys.CREATE_NEW_RECORD],
);
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD].label).toBe(
'Overridden Create Action',
);
});
it('should handle complex scenario with inheritance, overrides, and custom config', () => {
const customConfig: Record<string, ActionConfig> = {
'custom-action-1': {
type: ActionType.Standard,
scope: ActionScope.Object,
key: 'custom-action-1',
label: 'Custom Action 1',
position: 50,
Icon: IconPlus,
shouldBeRegistered: () => true,
component: MockComponent,
},
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
label: 'Completely Custom Favorites',
position: 1000,
Icon: IconHeart,
shouldBeRegistered: () => false,
component: MockComponent,
},
};
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
SingleRecordActionKeys.ADD_TO_FAVORITES,
SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
];
const propertiesToOverwrite = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {
label: 'Modified Create Label',
position: 5,
},
[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
isPinned: false,
},
};
const result = inheritActionsFromDefaultConfig({
config: customConfig,
actionKeys,
propertiesToOverwrite,
});
expect(Object.keys(result)).toHaveLength(4);
expect(result['custom-action-1']).toEqual(customConfig['custom-action-1']);
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]).toEqual({
...DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
label: 'Modified Create Label',
position: 5,
});
expect(result[SingleRecordActionKeys.ADD_TO_FAVORITES]).toEqual(
customConfig[SingleRecordActionKeys.ADD_TO_FAVORITES],
);
expect(result[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]).toEqual({
...DEFAULT_RECORD_ACTIONS_CONFIG[
SingleRecordActionKeys.REMOVE_FROM_FAVORITES
],
isPinned: false,
});
});
it('should handle empty overrides gracefully', () => {
const actionKeys: DefaultRecordActionConfigKeys[] = [
NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
];
const propertiesToOverwrite = {
[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]: {},
};
const result = inheritActionsFromDefaultConfig({
config: {},
actionKeys,
propertiesToOverwrite,
});
expect(result[NoSelectionRecordActionKeys.CREATE_NEW_RECORD]).toEqual(
DEFAULT_RECORD_ACTIONS_CONFIG[
NoSelectionRecordActionKeys.CREATE_NEW_RECORD
],
);
});
});

View File

@ -1,6 +1,5 @@
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { Key } from 'ts-key-enum';
import { Button } from 'twenty-ui/input';
import { getOsControlSymbol } from 'twenty-ui/utilities';
@ -18,7 +17,6 @@ export const CmdEnterActionButton = ({
keys: [`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
callback: () => onClick(),
focusId: SIDE_PANEL_FOCUS_ID,
scope: AppHotkeyScope.CommandMenuOpen,
dependencies: [onClick],
});

View File

@ -7,12 +7,10 @@ import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useTheme } from '@emotion/react';
import { useContext } from 'react';
@ -39,7 +37,6 @@ export const CommandMenuActionMenuDropdown = () => {
dropdownComponentInstanceIdFromProps: dropdownId,
});
},
scope: AppHotkeyScope.CommandMenuOpen,
dependencies: [toggleDropdown],
};
@ -86,7 +83,6 @@ export const CommandMenuActionMenuDropdown = () => {
selectableListInstanceId={actionMenuId}
focusId={dropdownId}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{recordSelectionActions.map((action) => (
<ActionComponent action={action} key={action.key} />

View File

@ -10,7 +10,6 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@ -89,7 +88,6 @@ export const RecordIndexActionMenuDropdown = () => {
focusId={dropdownId}
selectableItemIdArray={selectedItemIdArray}
selectableListInstanceId={dropdownId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{recordIndexActions.map((action) => (
<ActionComponent action={action} key={action.key} />

View File

@ -13,7 +13,6 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@ -123,7 +122,6 @@ export const RecordShowRightDrawerOpenRecordButton = ({
keys: ['ctrl+Enter,meta+Enter'],
callback: handleOpenRecord,
focusId: SIDE_PANEL_FOCUS_ID,
scope: AppHotkeyScope.CommandMenuOpen,
dependencies: [handleOpenRecord],
});