Replace hotkey scopes by focus stack (Part 5 - Form field Inputs, Pages, Dialog ...) (#13106)

# Replace hotkey scopes by focus stack (Part 5 - Form field Inputs,
Pages, Dialog ...)

This PR is the 5th 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

In this part, all the last components using hotkey scopes were
refactored.
In the 6th and final part of this refactoring we will be able to
completely remove the hotkey scopes from the codebase.
This commit is contained in:
Raphaël Bosi
2025-07-08 20:18:32 +02:00
committed by GitHub
parent 66b633e08e
commit 9eaa8ad517
50 changed files with 590 additions and 678 deletions

View File

@ -1,26 +0,0 @@
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useEffect } from 'react';
/**
* @deprecated This hook uses useEffect
* Use event handlers and imperative code to manage hotkey scope changes.
*/
export const useHotkeyScopeOnMount = (hotkeyScope: string) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: hotkeyScope,
});
return () => {
goBackToPreviousHotkeyScope();
};
}, [
hotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
]);
};

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
@ -11,7 +11,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly'; import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
@ -24,9 +23,6 @@ import { Task } from '@/activities/types/Task';
import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore'; import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore';
import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete'; import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete';
import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore'; import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
@ -39,6 +35,7 @@ import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import type { PartialBlock } from '@blocknote/core'; import type { PartialBlock } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css'; import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
@ -304,24 +301,17 @@ export const ActivityRichTextEditor = ({
uploadFile: handleEditorBuiltInUploadFile, uploadFile: handleEditorBuiltInUploadFile,
}); });
const commandMenuPage = useRecoilValue(commandMenuPageState); useHotkeysOnFocusedElement({
keys: Key.Escape,
useScopedHotkeys( callback: () => {
Key.Escape,
() => {
editor.domElement?.blur(); editor.domElement?.blur();
}, },
ActivityEditorHotkeyScope.ActivityBody, focusId: activityId,
); scope: ActivityEditorHotkeyScope.ActivityBody,
dependencies: [editor],
useScopedHotkeys( });
'*',
(keyboardEvent) => {
// TODO: remove once stacked hotkeys / focusKeys are in place
if (commandMenuPage !== CommandMenuPages.EditRichText) {
return;
}
const handleAllKeys = (keyboardEvent: KeyboardEvent) => {
if (keyboardEvent.key === Key.Escape) { if (keyboardEvent.key === Key.Escape) {
return; return;
} }
@ -351,13 +341,16 @@ export const ActivityRichTextEditor = ({
editor.setTextCursorPosition(newBlockId, 'end'); editor.setTextCursorPosition(newBlockId, 'end');
editor.focus(); editor.focus();
}, };
CommandMenuHotkeyScope.CommandMenuFocused,
[], useHotkeysOnFocusedElement({
{ keys: '*',
preventDefault: false, callback: handleAllKeys,
}, focusId: activityId,
); scope: ActivityEditorHotkeyScope.ActivityBody,
dependencies: [handleAllKeys],
});
const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({ const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({
objectNameSingular: activityObjectNameSingular, objectNameSingular: activityObjectNameSingular,
objectRecordId: activityId, objectRecordId: activityId,

View File

@ -31,8 +31,10 @@ import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/use
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { AppBasePath } from '@/types/AppBasePath'; import { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { PageFocusId } from '@/types/PageFocusId';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { AnalyticsType } from '~/generated/graphql'; import { AnalyticsType } from '~/generated/graphql';
@ -48,8 +50,6 @@ export const PageChangeEffect = () => {
const [previousLocation, setPreviousLocation] = useState(''); const [previousLocation, setPreviousLocation] = useState('');
const setHotkeyScope = useSetHotkeyScope();
const location = useLocation(); const location = useLocation();
const pageChangeEffectNavigateLocation = const pageChangeEffectNavigateLocation =
@ -92,6 +92,8 @@ export const PageChangeEffect = () => {
const { closeCommandMenu } = useCommandMenu(); const { closeCommandMenu } = useCommandMenu();
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex(); const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex();
useEffect(() => { useEffect(() => {
@ -140,55 +142,200 @@ export const PageChangeEffect = () => {
break; break;
} }
case isMatchingLocation(location, AppPath.RecordShowPage): { case isMatchingLocation(location, AppPath.RecordShowPage): {
setHotkeyScope(PageHotkeyScope.RecordShowPage, { resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.RecordShowPage,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.RecordShowPage,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: true,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.RecordShowPage,
customScopes: {
goto: true, goto: true,
keyboardShortcutMenu: true, keyboardShortcutMenu: true,
searchRecords: true, searchRecords: true,
},
},
}); });
break; break;
} }
case isMatchingLocation(location, AppPath.SignInUp): { case isMatchingLocation(location, AppPath.SignInUp): {
setHotkeyScope(PageHotkeyScope.SignInUp); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.SignInUp,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.SignInUp,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.SignInUp,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.Invite): { case isMatchingLocation(location, AppPath.Invite): {
setHotkeyScope(PageHotkeyScope.SignInUp); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.InviteTeam,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.InviteTeam,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.InviteTeam,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.CreateProfile): { case isMatchingLocation(location, AppPath.CreateProfile): {
setHotkeyScope(PageHotkeyScope.CreateProfile); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.CreateProfile,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.CreateProfile,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.CreateProfile,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.CreateWorkspace): { case isMatchingLocation(location, AppPath.CreateWorkspace): {
setHotkeyScope(PageHotkeyScope.CreateWorkspace); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.CreateWorkspace,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.CreateWorkspace,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.CreateWorkspace,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.SyncEmails): { case isMatchingLocation(location, AppPath.SyncEmails): {
setHotkeyScope(PageHotkeyScope.SyncEmail); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.SyncEmail,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.SyncEmail,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.SyncEmail,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.InviteTeam): { case isMatchingLocation(location, AppPath.InviteTeam): {
setHotkeyScope(PageHotkeyScope.InviteTeam); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.InviteTeam,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.InviteTeam,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.InviteTeam,
},
});
break; break;
} }
case isMatchingLocation(location, AppPath.PlanRequired): { case isMatchingLocation(location, AppPath.PlanRequired): {
setHotkeyScope(PageHotkeyScope.PlanRequired); resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.PlanRequired,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.PlanRequired,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.PlanRequired,
},
});
break; break;
} }
case location.pathname.startsWith(AppBasePath.Settings): { case location.pathname.startsWith(AppBasePath.Settings): {
setHotkeyScope(PageHotkeyScope.Settings, { resetFocusStackToFocusItem({
focusStackItem: {
focusId: PageFocusId.Settings,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: PageFocusId.Settings,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysConflictingWithKeyboard: false,
},
memoizeKey: 'global',
},
hotkeyScope: {
scope: PageHotkeyScope.Settings,
customScopes: {
goto: false, goto: false,
keyboardShortcutMenu: false, keyboardShortcutMenu: false,
commandMenu: false, commandMenu: false,
commandMenuOpen: false, commandMenuOpen: false,
searchRecords: false, searchRecords: false,
},
},
}); });
break; break;
} }
} }
}, [ }, [
location, location,
setHotkeyScope,
previousLocation, previousLocation,
contextStoreCurrentViewType, contextStoreCurrentViewType,
resetTableSelections, resetTableSelections,
@ -198,6 +345,7 @@ export const PageChangeEffect = () => {
deactivateBoardCard, deactivateBoardCard,
unfocusBoardCard, unfocusBoardCard,
resetFocusStackToRecordIndex, resetFocusStackToRecordIndex,
resetFocusStackToFocusItem,
]); ]);
useEffect(() => { useEffect(() => {

View File

@ -4,9 +4,12 @@ import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/K
import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable'; import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu'; import {
KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
useKeyboardShortcutMenu,
} from '../hooks/useKeyboardShortcutMenu';
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog'; import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup'; import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem'; import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
@ -15,15 +18,15 @@ export const KeyboardShortcutMenuOpenContent = () => {
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } = const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
useKeyboardShortcutMenu(); useKeyboardShortcutMenu();
useGlobalHotkeys( useHotkeysOnFocusedElement({
[Key.Escape], keys: [Key.Escape],
() => { callback: () => {
closeKeyboardShortcutMenu(); closeKeyboardShortcutMenu();
}, },
false, focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
AppHotkeyScope.KeyboardShortcutMenuOpen, scope: AppHotkeyScope.KeyboardShortcutMenuOpen,
[closeKeyboardShortcutMenu], dependencies: [closeKeyboardShortcutMenu],
); });
return ( return (
<> <>

View File

@ -8,18 +8,24 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useKeyboardShortcutMenu } from '../useKeyboardShortcutMenu'; import { useKeyboardShortcutMenu } from '../useKeyboardShortcutMenu';
const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn(); const mockPushFocusItemToFocusStack = jest.fn();
const mockRemoveFocusItemFromFocusStackById = jest.fn();
const mockGoBackToPreviousHotkeyScope = jest.fn(); jest.mock('@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack', () => ({
usePushFocusItemToFocusStack: () => ({
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({ pushFocusItemToFocusStack: mockPushFocusItemToFocusStack,
usePreviousHotkeyScope: () => ({
setHotkeyScopeAndMemorizePreviousScope:
mockSetHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
}), }),
})); }));
jest.mock(
'@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById',
() => ({
useRemoveFocusItemFromFocusStackById: () => ({
removeFocusItemFromFocusStackById: mockRemoveFocusItemFromFocusStackById,
}),
}),
);
const renderHookConfig = () => { const renderHookConfig = () => {
const { result } = renderHook( const { result } = renderHook(
() => { () => {
@ -39,6 +45,10 @@ const renderHookConfig = () => {
}; };
describe('useKeyboardShortcutMenu', () => { describe('useKeyboardShortcutMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should toggle keyboard shortcut menu correctly', async () => { it('should toggle keyboard shortcut menu correctly', async () => {
const { result } = renderHookConfig(); const { result } = renderHookConfig();
expect(result.current.toggleKeyboardShortcutMenu).toBeDefined(); expect(result.current.toggleKeyboardShortcutMenu).toBeDefined();
@ -48,8 +58,19 @@ describe('useKeyboardShortcutMenu', () => {
result.current.toggleKeyboardShortcutMenu(); result.current.toggleKeyboardShortcutMenu();
}); });
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ expect(mockPushFocusItemToFocusStack).toHaveBeenCalledWith({
scope: AppHotkeyScope.KeyboardShortcutMenu, focusId: 'keyboard-shortcut-menu',
component: {
type: 'keyboard-shortcut-menu',
instanceId: 'keyboard-shortcut-menu',
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
enableGlobalHotkeysWithModifiers: false,
},
hotkeyScope: {
scope: AppHotkeyScope.KeyboardShortcutMenuOpen,
},
}); });
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
@ -57,8 +78,8 @@ describe('useKeyboardShortcutMenu', () => {
result.current.toggleKeyboardShortcutMenu(); result.current.toggleKeyboardShortcutMenu();
}); });
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ expect(mockRemoveFocusItemFromFocusStackById).toHaveBeenCalledWith({
scope: AppHotkeyScope.KeyboardShortcutMenu, focusId: 'keyboard-shortcut-menu',
}); });
expect(result.current.isKeyboardShortcutMenuOpened).toBe(false); expect(result.current.isKeyboardShortcutMenuOpened).toBe(false);
}); });
@ -69,8 +90,19 @@ describe('useKeyboardShortcutMenu', () => {
result.current.openKeyboardShortcutMenu(); result.current.openKeyboardShortcutMenu();
}); });
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith({ expect(mockPushFocusItemToFocusStack).toHaveBeenCalledWith({
scope: AppHotkeyScope.KeyboardShortcutMenu, focusId: 'keyboard-shortcut-menu',
component: {
type: 'keyboard-shortcut-menu',
instanceId: 'keyboard-shortcut-menu',
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
enableGlobalHotkeysWithModifiers: false,
},
hotkeyScope: {
scope: AppHotkeyScope.KeyboardShortcutMenuOpen,
},
}); });
expect(result.current.isKeyboardShortcutMenuOpened).toBe(true); expect(result.current.isKeyboardShortcutMenuOpened).toBe(true);
@ -78,7 +110,9 @@ describe('useKeyboardShortcutMenu', () => {
result.current.closeKeyboardShortcutMenu(); result.current.closeKeyboardShortcutMenu();
}); });
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled(); expect(mockRemoveFocusItemFromFocusStackById).toHaveBeenCalledWith({
focusId: 'keyboard-shortcut-menu',
});
expect(result.current.isKeyboardShortcutMenuOpened).toBe(false); expect(result.current.isKeyboardShortcutMenuOpened).toBe(false);
}); });
}); });

View File

@ -1,25 +1,39 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState'; import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
export const KEYBOARD_SHORTCUT_MENU_INSTANCE_ID = 'keyboard-shortcut-menu';
export const useKeyboardShortcutMenu = () => { export const useKeyboardShortcutMenu = () => {
const { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
setHotkeyScopeAndMemorizePreviousScope, const { removeFocusItemFromFocusStackById } =
goBackToPreviousHotkeyScope, useRemoveFocusItemFromFocusStackById();
} = usePreviousHotkeyScope();
const openKeyboardShortcutMenu = useRecoilCallback( const openKeyboardShortcutMenu = useRecoilCallback(
({ set }) => ({ set }) =>
() => { () => {
set(isKeyboardShortcutMenuOpenedState, true); set(isKeyboardShortcutMenuOpenedState, true);
setHotkeyScopeAndMemorizePreviousScope({ pushFocusItemToFocusStack({
scope: AppHotkeyScope.KeyboardShortcutMenu, focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
component: {
type: FocusComponentType.KEYBOARD_SHORTCUT_MENU,
instanceId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
enableGlobalHotkeysWithModifiers: false,
},
hotkeyScope: {
scope: AppHotkeyScope.KeyboardShortcutMenuOpen,
},
}); });
}, },
[setHotkeyScopeAndMemorizePreviousScope], [pushFocusItemToFocusStack],
); );
const closeKeyboardShortcutMenu = useRecoilCallback( const closeKeyboardShortcutMenu = useRecoilCallback(
@ -31,10 +45,12 @@ export const useKeyboardShortcutMenu = () => {
if (isKeyboardShortcutMenuOpened) { if (isKeyboardShortcutMenuOpened) {
set(isKeyboardShortcutMenuOpenedState, false); set(isKeyboardShortcutMenuOpenedState, false);
goBackToPreviousHotkeyScope(); removeFocusItemFromFocusStackById({
focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
});
} }
}, },
[goBackToPreviousHotkeyScope], [removeFocusItemFromFocusStackById],
); );
const toggleKeyboardShortcutMenu = useRecoilCallback( const toggleKeyboardShortcutMenu = useRecoilCallback(

View File

@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation'; import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation';
import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys'; import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys';
import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { PageFocusId } from '@/types/PageFocusId';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
export const RecordBoardHotkeyEffect = () => { export const RecordBoardHotkeyEffect = () => {
@ -18,7 +18,7 @@ export const RecordBoardHotkeyEffect = () => {
callback: () => { callback: () => {
move('down'); move('down');
}, },
focusId: RECORD_INDEX_FOCUS_ID, focusId: PageFocusId.RecordIndex,
scope: RecordIndexHotkeyScope.RecordIndex, scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move], dependencies: [move],
}); });

View File

@ -1,49 +1,15 @@
import styled from '@emotion/styled';
import { useCallback, useRef } from 'react';
import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions'; import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { ViewType } from '@/views/types/ViewType'; import { ViewType } from '@/views/types/ViewType';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
const StyledMenuContainer = styled.div` export const RecordBoardColumnDropdownMenu = () => {
position: absolute;
top: ${({ theme }) => theme.spacing(10)};
width: 200px;
z-index: 1;
`;
type RecordBoardColumnDropdownMenuProps = {
onClose: () => void;
onDelete?: (id: string) => void;
stageId: string;
};
export const RecordBoardColumnDropdownMenu = ({
onClose,
}: RecordBoardColumnDropdownMenuProps) => {
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
const recordGroupActions = useRecordGroupActions({ const recordGroupActions = useRecordGroupActions({
viewType: ViewType.Kanban, viewType: ViewType.Kanban,
}); });
const closeMenu = useCallback(() => {
onClose();
}, [onClose]);
useListenClickOutside({
refs: [boardColumnMenuRef],
callback: closeMenu,
listenerId: 'record-board-column-dropdown-menu',
});
return ( return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<OverlayContainer>
<DropdownContent selectDisabled> <DropdownContent selectDisabled>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{recordGroupActions.map((action) => ( {recordGroupActions.map((action) => (
@ -51,7 +17,6 @@ export const RecordBoardColumnDropdownMenu = ({
key={action.id} key={action.id}
onClick={() => { onClick={() => {
action.callback(); action.callback();
closeMenu();
}} }}
LeftIcon={action.icon} LeftIcon={action.icon}
text={action.label} text={action.label}
@ -59,7 +24,5 @@ export const RecordBoardColumnDropdownMenu = ({
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownContent> </DropdownContent>
</OverlayContainer>
</StyledMenuContainer>
); );
}; };

View File

@ -7,10 +7,10 @@ import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/reco
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { Tag } from 'twenty-ui/components'; import { Tag } from 'twenty-ui/components';
import { IconDotsVertical, IconPlus } from 'twenty-ui/display'; import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input'; import { LightIconButton } from 'twenty-ui/input';
@ -66,32 +66,11 @@ const StyledTag = styled(Tag)`
export const RecordBoardColumnHeader = () => { export const RecordBoardColumnHeader = () => {
const { columnDefinition } = useContext(RecordBoardColumnContext); const { columnDefinition } = useContext(RecordBoardColumnContext);
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false); const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem, selectFieldMetadataItem } = const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext); useContext(RecordBoardContext);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleBoardColumnMenuOpen = () => {
setIsBoardColumnMenuOpen(true);
setHotkeyScopeAndMemorizePreviousScope({
scope: RecordBoardColumnHotkeyScope.BoardColumn,
customScopes: {
goto: false,
},
});
};
const handleBoardColumnMenuClose = () => {
goBackToPreviousHotkeyScope();
setIsBoardColumnMenuOpen(false);
};
const { aggregateValue, aggregateLabel } = const { aggregateValue, aggregateLabel } =
useAggregateRecordsForRecordBoardColumn(); useAggregateRecordsForRecordBoardColumn();
@ -105,6 +84,10 @@ export const RecordBoardColumnHeader = () => {
objectMetadataItem: objectMetadataItem, objectMetadataItem: objectMetadataItem,
}); });
const { toggleDropdown } = useToggleDropdown();
const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`;
return ( return (
<StyledColumn> <StyledColumn>
<StyledHeader <StyledHeader
@ -113,8 +96,15 @@ export const RecordBoardColumnHeader = () => {
> >
<StyledHeaderContainer> <StyledHeaderContainer>
<StyledLeftContainer> <StyledLeftContainer>
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="bottom-start"
dropdownOffset={{
x: 0,
y: 10,
}}
clickableComponent={
<StyledTag <StyledTag
onClick={handleBoardColumnMenuOpen}
variant={ variant={
columnDefinition.type === RecordGroupDefinitionType.Value columnDefinition.type === RecordGroupDefinitionType.Value
? 'solid' ? 'solid'
@ -132,6 +122,10 @@ export const RecordBoardColumnHeader = () => {
: 'medium' : 'medium'
} }
/> />
}
dropdownComponents={<RecordBoardColumnDropdownMenu />}
/>
<RecordBoardColumnHeaderAggregateDropdown <RecordBoardColumnHeaderAggregateDropdown
aggregateValue={aggregateValue} aggregateValue={aggregateValue}
dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`} dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`}
@ -145,7 +139,11 @@ export const RecordBoardColumnHeader = () => {
<LightIconButton <LightIconButton
accent="tertiary" accent="tertiary"
Icon={IconDotsVertical} Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen} onClick={() => {
toggleDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
}}
/> />
{hasObjectUpdatePermissions && ( {hasObjectUpdatePermissions && (
<LightIconButton <LightIconButton
@ -164,12 +162,6 @@ export const RecordBoardColumnHeader = () => {
</StyledRightContainer> </StyledRightContainer>
</StyledHeaderContainer> </StyledHeaderContainer>
</StyledHeader> </StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
stageId={columnDefinition.id}
/>
)}
</StyledColumn> </StyledColumn>
); );
}; };

View File

@ -1,36 +1,24 @@
import { Key } from 'ts-key-enum';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement'; import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { import {
RecordBoardColumnHeaderAggregateDropdownContext, RecordBoardColumnHeaderAggregateDropdownContext,
RecordBoardColumnHeaderAggregateDropdownContextValue, RecordBoardColumnHeaderAggregateDropdownContextValue,
} from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext'; } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
const { t } = useLingui(); const { t } = useLingui();
const { onContentChange, closeDropdown } = const { onContentChange } =
useDropdownContextStateManagement<RecordBoardColumnHeaderAggregateDropdownContextValue>( useDropdownContextStateManagement<RecordBoardColumnHeaderAggregateDropdownContextValue>(
{ {
context: RecordBoardColumnHeaderAggregateDropdownContext, context: RecordBoardColumnHeaderAggregateDropdownContext,
}, },
); );
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
return ( return (
<DropdownContent> <DropdownContent>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>

View File

@ -10,18 +10,15 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate'; import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconCheck, IconChevronLeft } from 'twenty-ui/display'; import { IconCheck, IconChevronLeft } from 'twenty-ui/display';
export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
@ -38,14 +35,6 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
}, },
); );
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const setAggregateOperation = useSetRecoilComponentStateV2( const setAggregateOperation = useSetRecoilComponentStateV2(
aggregateOperationComponentState, aggregateOperationComponentState,
); );

View File

@ -85,6 +85,7 @@ export const FormBooleanFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (

View File

@ -296,6 +296,7 @@ export const FormDateTimeFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<StyledInputContainer <StyledInputContainer
formFieldInputInstanceId={instanceId}
ref={datePickerWrapperRef} ref={datePickerWrapperRef}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
> >

View File

@ -1,5 +1,7 @@
import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope'; import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { forwardRef, HTMLAttributes, Ref } from 'react'; import { forwardRef, HTMLAttributes, Ref } from 'react';
@ -9,9 +11,12 @@ type FormFieldInputInnerContainerProps = {
multiline?: boolean; multiline?: boolean;
readonly?: boolean; readonly?: boolean;
preventSetHotkeyScope?: boolean; preventSetHotkeyScope?: boolean;
formFieldInputInstanceId: string;
}; };
const StyledFormFieldInputInnerContainer = styled.div<FormFieldInputInnerContainerProps>` const StyledFormFieldInputInnerContainer = styled.div<
Omit<FormFieldInputInnerContainerProps, 'formFieldInputInstanceId'>
>`
background-color: ${({ theme }) => theme.background.transparent.lighter}; background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm}; border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
@ -48,20 +53,25 @@ export const FormFieldInputInnerContainer = forwardRef(
readonly, readonly,
preventSetHotkeyScope = false, preventSetHotkeyScope = false,
onClick, onClick,
formFieldInputInstanceId,
}: HTMLAttributes<HTMLDivElement> & FormFieldInputInnerContainerProps, }: HTMLAttributes<HTMLDivElement> & FormFieldInputInnerContainerProps,
ref: Ref<HTMLDivElement>, ref: Ref<HTMLDivElement>,
) => { ) => {
const { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
goBackToPreviousHotkeyScope, const { removeFocusItemFromFocusStackById } =
setHotkeyScopeAndMemorizePreviousScope, useRemoveFocusItemFromFocusStackById();
} = usePreviousHotkeyScope();
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => { const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
onFocus?.(e); onFocus?.(e);
if (!preventSetHotkeyScope) { if (!preventSetHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope({ pushFocusItemToFocusStack({
scope: FormFieldInputHotKeyScope.FormFieldInput, focusId: formFieldInputInstanceId,
component: {
type: FocusComponentType.FORM_FIELD_INPUT,
instanceId: formFieldInputInstanceId,
},
hotkeyScope: { scope: FormFieldInputHotKeyScope.FormFieldInput },
}); });
} }
}; };
@ -70,7 +80,9 @@ export const FormFieldInputInnerContainer = forwardRef(
onBlur?.(e); onBlur?.(e);
if (!preventSetHotkeyScope) { if (!preventSetHotkeyScope) {
goBackToPreviousHotkeyScope(); removeFocusItemFromFocusStackById({
focusId: formFieldInputInstanceId,
});
} }
}; };

View File

@ -14,7 +14,9 @@ import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel'; import { InputLabel } from '@/ui/input/components/InputLabel';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { isArray } from '@sniptt/guards'; import { isArray } from '@sniptt/guards';
@ -88,10 +90,9 @@ export const FormMultiSelectFieldInput = ({
const hotkeyScope = const hotkeyScope =
FormMultiSelectFieldInputHotKeyScope.FormMultiSelectFieldInput; FormMultiSelectFieldInputHotKeyScope.FormMultiSelectFieldInput;
const { const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
setHotkeyScopeAndMemorizePreviousScope, const { removeFocusItemFromFocusStackById } =
goBackToPreviousHotkeyScope, useRemoveFocusItemFromFocusStackById();
} = usePreviousHotkeyScope();
const [draftValue, setDraftValue] = useState< const [draftValue, setDraftValue] = useState<
| { | {
@ -128,8 +129,13 @@ export const FormMultiSelectFieldInput = ({
editingMode: 'edit', editingMode: 'edit',
}); });
setHotkeyScopeAndMemorizePreviousScope({ pushFocusItemToFocusStack({
scope: hotkeyScope, focusId: instanceId,
component: {
type: FocusComponentType.FORM_FIELD_INPUT,
instanceId,
},
hotkeyScope: { scope: hotkeyScope },
}); });
}; };
@ -157,7 +163,7 @@ export const FormMultiSelectFieldInput = ({
editingMode: 'view', editingMode: 'view',
}); });
goBackToPreviousHotkeyScope(); removeFocusItemFromFocusStackById({ focusId: instanceId });
}; };
const handleVariableTagInsert = (variableName: string) => { const handleVariableTagInsert = (variableName: string) => {
@ -201,6 +207,7 @@ export const FormMultiSelectFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (

View File

@ -121,6 +121,7 @@ export const FormNumberFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
onBlur={onBlur} onBlur={onBlur}
> >

View File

@ -71,6 +71,7 @@ export const FormRawJsonFieldInput = ({
<FormFieldInputRowContainer multiline> <FormFieldInputRowContainer multiline>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
multiline multiline
onBlur={onBlur} onBlur={onBlur}

View File

@ -7,8 +7,8 @@ import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/
import { InputLabel } from '@/ui/input/components/InputLabel'; import { InputLabel } from '@/ui/input/components/InputLabel';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useId, useState } from 'react'; import { useId, useState } from 'react';
@ -40,7 +40,8 @@ export const FormSelectFieldInput = ({
const hotkeyScope = InlineCellHotkeyScope.InlineCell; const hotkeyScope = InlineCellHotkeyScope.InlineCell;
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const [draftValue, setDraftValue] = useState< const [draftValue, setDraftValue] = useState<
| { | {
@ -72,7 +73,7 @@ export const FormSelectFieldInput = ({
editingMode: 'view', editingMode: 'view',
}); });
goBackToPreviousHotkeyScope(); removeFocusItemFromFocusStackById({ focusId: instanceId });
onChange(option); onChange(option);
}; };
@ -87,7 +88,7 @@ export const FormSelectFieldInput = ({
editingMode: 'view', editingMode: 'view',
}); });
goBackToPreviousHotkeyScope(); removeFocusItemFromFocusStackById({ focusId: instanceId });
}; };
const selectedOption = options.find( const selectedOption = options.find(
@ -119,14 +120,13 @@ export const FormSelectFieldInput = ({
onChange(variableName); onChange(variableName);
}; };
useScopedHotkeys( useHotkeysOnFocusedElement({
Key.Escape, keys: Key.Escape,
() => { callback: onCancel,
onCancel(); focusId: instanceId,
}, scope: hotkeyScope,
hotkeyScope, dependencies: [onCancel],
[onCancel], });
);
return ( return (
<FormFieldInputContainer> <FormFieldInputContainer>
@ -149,6 +149,7 @@ export const FormSelectFieldInput = ({
/> />
) : ( ) : (
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
<VariableChipStandalone <VariableChipStandalone

View File

@ -150,7 +150,11 @@ export const FormSingleRecordPicker = ({
{label ? <InputLabel>{label}</InputLabel> : null} {label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
{disabled ? ( {disabled ? (
<StyledFormSelectContainer hasRightElement={false} readonly> <StyledFormSelectContainer
formFieldInputInstanceId={componentId}
hasRightElement={false}
readonly
>
<FormSingleRecordFieldChip <FormSingleRecordFieldChip
draftValue={draftValue} draftValue={draftValue}
selectedRecord={selectedRecord} selectedRecord={selectedRecord}
@ -169,6 +173,7 @@ export const FormSingleRecordPicker = ({
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }} dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
clickableComponent={ clickableComponent={
<StyledFormSelectContainer <StyledFormSelectContainer
formFieldInputInstanceId={componentId}
hasRightElement={isDefined(VariablePicker) && !disabled} hasRightElement={isDefined(VariablePicker) && !disabled}
preventSetHotkeyScope={true} preventSetHotkeyScope={true}
> >

View File

@ -71,6 +71,7 @@ export const FormTextFieldInput = ({
<FormFieldInputRowContainer multiline={multiline}> <FormFieldInputRowContainer multiline={multiline}>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
multiline={multiline} multiline={multiline}
onBlur={onBlur} onBlur={onBlur}

View File

@ -95,6 +95,7 @@ export const FormUuidFieldInput = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly} hasRightElement={isDefined(VariablePicker) && !readonly}
> >
{draftValue.type === 'static' ? ( {draftValue.type === 'static' ? (

View File

@ -1 +0,0 @@
export const RECORD_INDEX_FOCUS_ID = 'record-index';

View File

@ -1,5 +1,5 @@
import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { PageFocusId } from '@/types/PageFocusId';
import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem'; import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -9,10 +9,10 @@ export const useResetFocusStackToRecordIndex = () => {
const resetFocusStackToRecordIndex = () => { const resetFocusStackToRecordIndex = () => {
resetFocusStackToFocusItem({ resetFocusStackToFocusItem({
focusStackItem: { focusStackItem: {
focusId: RECORD_INDEX_FOCUS_ID, focusId: PageFocusId.RecordIndex,
componentInstance: { componentInstance: {
componentType: FocusComponentType.PAGE, componentType: FocusComponentType.PAGE,
componentInstanceId: RECORD_INDEX_FOCUS_ID, componentInstanceId: PageFocusId.RecordIndex,
}, },
globalHotkeysConfig: { globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: true, enableGlobalHotkeysWithModifiers: true,

View File

@ -14,7 +14,6 @@ import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/Recor
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext'; import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable'; import { useRecordTable } from '../hooks/useRecordTable';
@ -48,16 +47,6 @@ export const RecordTableWithWrappers = ({
selectAllRows(); selectAllRows();
}; };
useScopedHotkeys(
'ctrl+a,meta+a',
handleSelectAllRows,
RecordIndexHotkeyScope.RecordIndex,
[],
{
enableOnFormTags: false,
},
);
useHotkeysOnFocusedElement({ useHotkeysOnFocusedElement({
keys: ['ctrl+a,meta+a'], keys: ['ctrl+a,meta+a'],
callback: handleSelectAllRows, callback: handleSelectAllRows,

View File

@ -1,10 +1,10 @@
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector'; import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
import { PageFocusId } from '@/types/PageFocusId';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -28,7 +28,7 @@ export const RecordTableBodyEscapeHotkeyEffect = () => {
useHotkeysOnFocusedElement({ useHotkeysOnFocusedElement({
keys: [Key.Escape], keys: [Key.Escape],
callback: handleEscape, callback: handleEscape,
focusId: RECORD_INDEX_FOCUS_ID, focusId: PageFocusId.RecordIndex,
scope: RecordIndexHotkeyScope.RecordIndex, scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [handleEscape], dependencies: [handleEscape],
options: { options: {

View File

@ -1,10 +1,10 @@
import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useRecordTableRowFocusHotkeys } from '@/object-record/record-table/hooks/useRecordTableRowFocusHotkeys'; import { useRecordTableRowFocusHotkeys } from '@/object-record/record-table/hooks/useRecordTableRowFocusHotkeys';
import { PageFocusId } from '@/types/PageFocusId';
export const RecordTableBodyFocusKeyboardEffect = () => { export const RecordTableBodyFocusKeyboardEffect = () => {
useRecordTableRowFocusHotkeys({ useRecordTableRowFocusHotkeys({
focusId: RECORD_INDEX_FOCUS_ID, focusId: PageFocusId.RecordIndex,
hotkeyScope: RecordIndexHotkeyScope.RecordIndex, hotkeyScope: RecordIndexHotkeyScope.RecordIndex,
}); });

View File

@ -1,15 +1,11 @@
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems'; import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext } from 'react'; import { useContext } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui/display'; import { IconChevronLeft } from 'twenty-ui/display';
export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
@ -19,19 +15,10 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
aggregateOperations: ExtendedAggregateOperations[]; aggregateOperations: ExtendedAggregateOperations[];
title: string; title: string;
}) => { }) => {
const { dropdownId, resetContent } = useContext( const { resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext, RecordTableColumnAggregateFooterDropdownContext,
); );
const { closeDropdown } = useCloseDropdown();
useScopedHotkeys(
[Key.Escape],
() => {
resetContent();
closeDropdown(dropdownId);
},
TableOptionsHotkeyScope.Dropdown,
);
return ( return (
<DropdownContent> <DropdownContent>
<DropdownMenuHeader <DropdownMenuHeader

View File

@ -4,14 +4,11 @@ import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions'; import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation'; import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType'; import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display'; import { IconCheck } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
@ -28,14 +25,6 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
const { closeDropdown } = useCloseDropdown(); const { closeDropdown } = useCloseDropdown();
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown(dropdownId);
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperation = useMemo( const availableAggregateOperation = useMemo(
() => () =>
getAvailableAggregateOperationsForFieldMetadataType({ getAvailableAggregateOperationsForFieldMetadataType({

View File

@ -3,14 +3,10 @@ import {
SettingsServerlessFunctionCodeEditor, SettingsServerlessFunctionCodeEditor,
} from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor'; } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor';
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { import {
H2Title, H2Title,
IconGitCommit, IconGitCommit,
@ -19,8 +15,6 @@ import {
} from 'twenty-ui/display'; } from 'twenty-ui/display';
import { Button, CoreEditorHeader } from 'twenty-ui/input'; import { Button, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout'; import { Section } from 'twenty-ui/layout';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledTabList = styled(TabList)` const StyledTabList = styled(TabList)`
border-bottom: none; border-bottom: none;
@ -91,19 +85,6 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
/> />
); );
const navigate = useNavigateSettings();
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
);
useScopedHotkeys(
[Key.Escape],
() => {
navigate(SettingsPath.ServerlessFunctions);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
);
return ( return (
<Section> <Section>
<H2Title <H2Title

View File

@ -2,16 +2,12 @@ import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functio
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction'; import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
import { H2Title } from 'twenty-ui/display'; import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input'; import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout'; import { Section } from 'twenty-ui/layout';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal'; const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal';
@ -36,25 +32,6 @@ export const SettingsServerlessFunctionSettingsTab = ({
navigate(SettingsPath.ServerlessFunctions); navigate(SettingsPath.ServerlessFunctions);
}; };
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
);
useScopedHotkeys(
[Key.Delete],
() => {
openModal(DELETE_FUNCTION_MODAL_ID);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
);
useScopedHotkeys(
[Key.Escape],
() => {
navigate(SettingsPath.ServerlessFunctions);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
);
return ( return (
<> <>
<SettingsServerlessFunctionNewForm <SettingsServerlessFunctionNewForm

View File

@ -1,15 +1,9 @@
import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult'; import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
import { H2Title, IconPlayerPlay } from 'twenty-ui/display'; import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout'; import { Section } from 'twenty-ui/layout';
const StyledInputsContainer = styled.div` const StyledInputsContainer = styled.div`
@ -40,19 +34,6 @@ export const SettingsServerlessFunctionTestTab = ({
})); }));
}; };
const navigate = useNavigateSettings();
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab,
);
useScopedHotkeys(
[Key.Escape],
() => {
navigate(SettingsPath.ServerlessFunctions);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab,
);
return ( return (
<Section> <Section>
<H2Title <H2Title

View File

@ -0,0 +1,11 @@
export enum PageFocusId {
Settings = 'settings',
CreateWorkspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
InviteTeam = 'invite-team',
SyncEmail = 'sync-email',
PlanRequired = 'plan-required',
RecordShowPage = 'record-show-page',
RecordIndex = 'record-index',
}

View File

@ -2,11 +2,11 @@ import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { DIALOG_CLICK_OUTSIDE_ID } from '@/ui/feedback/dialog-manager/constants/DialogClickOutsideId'; import { DIALOG_CLICK_OUTSIDE_ID } from '@/ui/feedback/dialog-manager/constants/DialogClickOutsideId';
import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId'; import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices'; import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRef } from 'react'; import { useRef } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -96,9 +96,7 @@ export const Dialog = ({
closed: { y: '50vh' }, closed: { y: '50vh' },
}; };
useScopedHotkeys( const handleEnter = (event: KeyboardEvent) => {
Key.Enter,
(event: KeyboardEvent) => {
const confirmButton = buttons.find((button) => button.role === 'confirm'); const confirmButton = buttons.find((button) => button.role === 'confirm');
event.preventDefault(); event.preventDefault();
@ -107,20 +105,28 @@ export const Dialog = ({
confirmButton?.onClick?.(event); confirmButton?.onClick?.(event);
onClose?.(); onClose?.();
} }
}, };
DialogHotkeyScope.Dialog,
[],
);
useScopedHotkeys( const handleEscape = (event: KeyboardEvent) => {
Key.Escape,
(event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
onClose?.(); onClose?.();
}, };
DialogHotkeyScope.Dialog,
[], useHotkeysOnFocusedElement({
); keys: [Key.Enter],
callback: handleEnter,
focusId: DIALOG_FOCUS_ID,
scope: DialogHotkeyScope.Dialog,
dependencies: [buttons],
});
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handleEscape,
focusId: DIALOG_FOCUS_ID,
scope: DialogHotkeyScope.Dialog,
dependencies: [handleEscape],
});
const dialogRef = useRef<HTMLDivElement>(null); const dialogRef = useRef<HTMLDivElement>(null);

View File

@ -1,26 +1,34 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey'; import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey';
import { DialogHotkeyScope } from '@/ui/feedback/dialog-manager/types/DialogHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates'; import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
export const DialogManagerEffect = () => { export const DialogManagerEffect = () => {
const { dialogInternal } = useDialogManagerScopedStates(); const { dialogInternal } = useDialogManagerScopedStates();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
useEffect(() => { useEffect(() => {
if (dialogInternal.queue.length === 0) { if (dialogInternal.queue.length === 0) {
return; return;
} }
setHotkeyScopeAndMemorizePreviousScope({ pushFocusItemToFocusStack({
focusId: DIALOG_FOCUS_ID,
component: {
type: FocusComponentType.DIALOG,
instanceId: DIALOG_FOCUS_ID,
},
hotkeyScope: {
scope: DialogHotkeyScope.Dialog, scope: DialogHotkeyScope.Dialog,
},
memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY, memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY,
}); });
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]); }, [dialogInternal.queue, pushFocusItemToFocusStack]);
return <></>; return <></>;
}; };

View File

@ -0,0 +1 @@
export const DIALOG_FOCUS_ID = 'dialog';

View File

@ -1,10 +1,10 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/ui/feedback/dialog-manager/constants/DialogManagerHotkeyScopeMemoizeKey'; import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext'; import { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext';
import { dialogInternalScopedState } from '../states/dialogInternalScopedState'; import { dialogInternalScopedState } from '../states/dialogInternalScopedState';
import { DialogOptions } from '../types/DialogOptions'; import { DialogOptions } from '../types/DialogOptions';
@ -19,7 +19,8 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
props?.dialogManagerScopeId, props?.dialogManagerScopeId,
); );
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeDialog = useRecoilCallback( const closeDialog = useRecoilCallback(
({ set }) => ({ set }) =>
@ -29,9 +30,9 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
queue: prevState.queue.filter((dialog) => dialog.id !== id), queue: prevState.queue.filter((dialog) => dialog.id !== id),
})); }));
goBackToPreviousHotkeyScope(DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY); removeFocusItemFromFocusStackById({ focusId: DIALOG_FOCUS_ID });
}, },
[goBackToPreviousHotkeyScope, scopeId], [removeFocusItemFromFocusStackById, scopeId],
); );
const setDialogQueue = useRecoilCallback( const setDialogQueue = useRecoilCallback(

View File

@ -1,11 +1,13 @@
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { FocusEvent, useRef } from 'react'; import { FocusEvent, useRef } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconComponent, TablerIconsProps } from 'twenty-ui/display'; import { IconComponent, TablerIconsProps } from 'twenty-ui/display';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
type NavigationDrawerInputProps = { type NavigationDrawerInputProps = {
className?: string; className?: string;
@ -19,6 +21,8 @@ type NavigationDrawerInputProps = {
hotkeyScope: string; hotkeyScope: string;
}; };
const NAVIGATION_DRAWER_INPUT_FOCUS_ID = 'navigation-drawer-input';
export const NavigationDrawerInput = ({ export const NavigationDrawerInput = ({
className, className,
placeholder, placeholder,
@ -32,37 +36,64 @@ export const NavigationDrawerInput = ({
}: NavigationDrawerInputProps) => { }: NavigationDrawerInputProps) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useHotkeyScopeOnMount(hotkeyScope); useHotkeysOnFocusedElement({
keys: Key.Escape,
useScopedHotkeys( callback: () => {
[Key.Escape],
() => {
onCancel(value); onCancel(value);
removeFocusItemFromFocusStackById({
focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
});
}, },
hotkeyScope, focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
); scope: hotkeyScope,
});
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Enter], keys: Key.Enter,
() => { callback: () => {
onSubmit(value); onSubmit(value);
removeFocusItemFromFocusStackById({
focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
});
}, },
hotkeyScope, focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
); scope: hotkeyScope,
});
useListenClickOutside({ useListenClickOutside({
refs: [inputRef], refs: [inputRef],
callback: (event) => { callback: (event) => {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
onClickOutside(event, value); onClickOutside(event, value);
removeFocusItemFromFocusStackById({
focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
});
}, },
listenerId: 'navigation-drawer-input', listenerId: 'navigation-drawer-input',
}); });
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const handleFocus = (event: FocusEvent<HTMLInputElement>) => { const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
if (isDefined(value)) { if (isDefined(value)) {
event.target.select(); event.target.select();
} }
pushFocusItemToFocusStack({
focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
component: {
type: FocusComponentType.TEXT_INPUT,
instanceId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
},
hotkeyScope: { scope: hotkeyScope },
});
};
const handleBlur = () => {
removeFocusItemFromFocusStackById({
focusId: NAVIGATION_DRAWER_INPUT_FOCUS_ID,
});
}; };
return ( return (
@ -74,6 +105,7 @@ export const NavigationDrawerInput = ({
onChange={onChange} onChange={onChange}
placeholder={placeholder} placeholder={placeholder}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur}
sizeVariant="md" sizeVariant="md"
fullWidth fullWidth
autoFocus autoFocus

View File

@ -9,6 +9,9 @@ export enum FocusComponentType {
RECORD_TABLE_CELL = 'record-table-cell', RECORD_TABLE_CELL = 'record-table-cell',
TEXT_AREA = 'text-area', TEXT_AREA = 'text-area',
TEXT_INPUT = 'text-input', TEXT_INPUT = 'text-input',
FORM_FIELD_INPUT = 'form-field-input',
RECORD_BOARD_CARD = 'record-board-card', RECORD_BOARD_CARD = 'record-board-card',
ACTIVITY_RICH_TEXT_EDITOR = 'activity-rich-text-editor', ACTIVITY_RICH_TEXT_EDITOR = 'activity-rich-text-editor',
KEYBOARD_SHORTCUT_MENU = 'keyboard-shortcut-menu',
DIALOG = 'dialog',
} }

View File

@ -1 +1 @@
export const DEBUG_HOTKEY_SCOPE = true; export const DEBUG_HOTKEY_SCOPE = false;

View File

@ -1,32 +0,0 @@
import { act } from 'react-dom/test-utils';
import { fireEvent, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
const hotKeyCallback = jest.fn();
describe('useScopedHotkeys', () => {
it('should work as expected', () => {
renderHook(
() => {
useScopedHotkeys('ctrl+k', hotKeyCallback, AppHotkeyScope.App);
const setHotkeyScope = useSetHotkeyScope();
setHotkeyScope(AppHotkeyScope.App);
},
{
wrapper: RecoilRoot,
},
);
act(() => {
fireEvent.keyDown(document, { key: 'k', code: 'KeyK', ctrlKey: true });
});
expect(hotKeyCallback).toHaveBeenCalled();
});
});

View File

@ -1,80 +0,0 @@
import {
Hotkey,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilCallback } from 'recoil';
import { logDebug } from '~/utils/logDebug';
import { DEBUG_HOTKEY_SCOPE } from '../constants/DebugHotkeyScope';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
export const useScopedHotkeyCallback = (
dependencies?: OptionsOrDependencyArray,
) => {
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
return useRecoilCallback(
({ snapshot }) =>
({
callback,
hotkeysEvent,
keyboardEvent,
scope,
preventDefault,
}: {
keyboardEvent: KeyboardEvent;
hotkeysEvent: Hotkey;
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
scope: string;
preventDefault?: boolean;
}) => {
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.getValue();
if (!currentHotkeyScopes.includes(scope)) {
if (DEBUG_HOTKEY_SCOPE) {
logDebug(
`DEBUG: %cI can't call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: gray; ',
);
}
return;
}
if (DEBUG_HOTKEY_SCOPE) {
logDebug(
`DEBUG: %cI can call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: green;',
);
}
if (preventDefault === true) {
if (DEBUG_HOTKEY_SCOPE) {
logDebug(
`DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`,
'color: gray;',
);
}
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
keyboardEvent.stopImmediatePropagation();
}
return callback(keyboardEvent, hotkeysEvent);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
dependencyArray,
);
};

View File

@ -1,62 +0,0 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
export const useScopedHotkeys = (
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: unknown[],
options?: UseHotkeysOptionsWithoutBuggyOptions,
) => {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback(dependencies);
const enableOnContentEditable = isDefined(options?.enableOnContentEditable)
? options.enableOnContentEditable
: true;
const enableOnFormTags = isDefined(options?.enableOnFormTags)
? options.enableOnFormTags
: true;
const preventDefault = isDefined(options?.preventDefault)
? options.preventDefault === true
: true;
const ignoreModifiers = isDefined(options?.ignoreModifiers)
? options.ignoreModifiers === true
: false;
return useHotkeys(
keys,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
},
scope,
preventDefault,
});
},
{
enableOnContentEditable,
enableOnFormTags,
ignoreModifiers,
},
dependencies,
);
};

View File

@ -1,10 +1,10 @@
import { InputHotkeyScope } from '@/ui/input/types/InputHotkeyScope'; import { InputHotkeyScope } from '@/ui/input/types/InputHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role'; import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -117,17 +117,21 @@ export const useAgentChat = (agentId: string) => {
await sendChatMessage(content); await sendChatMessage(content);
}; };
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Enter], keys: [Key.Enter],
(event) => { callback: (event: KeyboardEvent) => {
if (!event.ctrlKey && !event.metaKey) { if (!event.ctrlKey && !event.metaKey) {
event.preventDefault(); event.preventDefault();
handleSendMessage(); handleSendMessage();
} }
}, },
InputHotkeyScope.TextInput, focusId: `${agentId}-chat-input`,
[agentChatInput, isLoading], scope: InputHotkeyScope.TextInput,
); dependencies: [agentChatInput, isLoading],
options: {
enableOnFormTags: true,
},
});
return { return {
handleInputChange: (value: string) => setAgentChatInput(value), handleInputChange: (value: string) => setAgentChatInput(value),

View File

@ -285,6 +285,7 @@ export const WorkflowEditActionFormBuilder = ({
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId={field.id}
hasRightElement={false} hasRightElement={false}
onClick={() => { onClick={() => {
handleFieldClick(field.id); handleFieldClick(field.id);
@ -358,6 +359,7 @@ export const WorkflowEditActionFormBuilder = ({
<FormFieldInputContainer> <FormFieldInputContainer>
<FormFieldInputRowContainer> <FormFieldInputRowContainer>
<FormFieldInputInnerContainer <FormFieldInputInnerContainer
formFieldInputInstanceId="add-field-button"
hasRightElement={false} hasRightElement={false}
onClick={() => { onClick={() => {
const { label, name } = getDefaultFormFieldSettings( const { label, name } = getDefaultFormFieldSettings(

View File

@ -47,7 +47,10 @@ export const WorkflowFormEmptyMessage = () => {
<StyledMessageContainer> <StyledMessageContainer>
<FormFieldInputContainer> <FormFieldInputContainer>
<FormFieldInputRowContainer multiline maxHeight={124}> <FormFieldInputRowContainer multiline maxHeight={124}>
<FormFieldInputInnerContainer hasRightElement={false}> <FormFieldInputInnerContainer
formFieldInputInstanceId="empty-form-message"
hasRightElement={false}
>
<StyledFieldContainer> <StyledFieldContainer>
<StyledMessageContentContainer> <StyledMessageContentContainer>
<StyledMessageTitle data-testid="empty-form-message-title"> <StyledMessageTitle data-testid="empty-form-message-title">

View File

@ -14,11 +14,12 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageFocusId } from '@/types/PageFocusId';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { ApolloError } from '@apollo/client'; import { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
@ -148,15 +149,23 @@ export const CreateProfile = () => {
const [isEditingMode, setIsEditingMode] = useState(false); const [isEditingMode, setIsEditingMode] = useState(false);
useScopedHotkeys( const handleEnter = () => {
Key.Enter, if (isEditingMode) {
() => { onSubmit(getValues());
}
};
useHotkeysOnFocusedElement({
keys: Key.Enter,
callback: () => {
if (isEditingMode) { if (isEditingMode) {
onSubmit(getValues()); onSubmit(getValues());
} }
}, },
PageHotkeyScope.CreateProfile, focusId: PageFocusId.CreateProfile,
); scope: PageHotkeyScope.CreateProfile,
dependencies: [handleEnter],
});
return ( return (
<Modal.Content isVerticalCentered isHorizontalCentered> <Modal.Content isVerticalCentered isHorizontalCentered>

View File

@ -3,11 +3,12 @@ import { Title } from '@/auth/components/Title';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { PageFocusId } from '@/types/PageFocusId';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -161,14 +162,15 @@ export const InviteTeam = () => {
await onSubmit({ emails: [] }); await onSubmit({ emails: [] });
}; };
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Enter], keys: Key.Enter,
() => { callback: () => {
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
}, },
PageHotkeyScope.InviteTeam, focusId: PageFocusId.InviteTeam,
[handleSubmit], scope: PageHotkeyScope.InviteTeam,
); dependencies: [handleSubmit, onSubmit],
});
return ( return (
<Modal.Content isVerticalCentered isHorizontalCentered> <Modal.Content isVerticalCentered isHorizontalCentered>

View File

@ -9,7 +9,6 @@ import { Title } from '@/auth/components/Title';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard'; import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState'; import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState'; import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
@ -17,7 +16,9 @@ import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicros
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState'; import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { PageFocusId } from '@/types/PageFocusId';
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ConnectedAccountProvider } from 'twenty-shared/types'; import { ConnectedAccountProvider } from 'twenty-shared/types';
import { IconGoogle, IconMicrosoft } from 'twenty-ui/display'; import { IconGoogle, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input'; import { MainButton } from 'twenty-ui/input';
@ -95,14 +96,15 @@ export const SyncEmails = () => {
const isMicrosoftProviderEnabled = const isMicrosoftProviderEnabled =
isMicrosoftMessagingEnabled || isMicrosoftCalendarEnabled; isMicrosoftMessagingEnabled || isMicrosoftCalendarEnabled;
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Enter], keys: Key.Enter,
async () => { callback: async () => {
await continueWithoutSync(); await continueWithoutSync();
}, },
PageHotkeyScope.SyncEmail, focusId: PageFocusId.SyncEmail,
[continueWithoutSync], scope: PageHotkeyScope.SyncEmail,
); dependencies: [continueWithoutSync],
});
return ( return (
<Modal.Content isVerticalCentered isHorizontalCentered> <Modal.Content isVerticalCentered isHorizontalCentered>

View File

@ -5,15 +5,11 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm'; import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction'; import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react'; import { useState } from 'react';
import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { isDefined } from 'twenty-shared/utils';
export const SettingsServerlessFunctionsNew = () => { export const SettingsServerlessFunctionsNew = () => {
const navigate = useNavigateSettings(); const navigate = useNavigateSettings();
@ -50,28 +46,6 @@ export const SettingsServerlessFunctionsNew = () => {
const canSave = !!formValues.name && createOneServerlessFunction; const canSave = !!formValues.name && createOneServerlessFunction;
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew,
);
useScopedHotkeys(
[Key.Enter],
() => {
if (canSave !== false) {
handleSave();
}
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew,
[canSave],
);
useScopedHotkeys(
[Key.Escape],
() => {
navigate(SettingsPath.ServerlessFunctions);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionNew,
);
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title="New Function" title="New Function"

View File

@ -4,21 +4,15 @@ import { RecoilRoot } from 'recoil';
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider'; import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
import { InitializeHotkeyStorybookHookEffect } from '../InitializeHotkeyStorybookHook';
import { mockedApolloClient } from '../mockedApolloClient'; import { mockedApolloClient } from '../mockedApolloClient';
export const RootDecorator: Decorator = (Story, context) => { export const RootDecorator: Decorator = (Story, context) => {
const { parameters } = context; const { parameters } = context;
const disableHotkeyInitialization = parameters.disableHotkeyInitialization;
return ( return (
<RecoilRoot initializeState={parameters.initializeState}> <RecoilRoot initializeState={parameters.initializeState}>
<ApolloProvider client={mockedApolloClient}> <ApolloProvider client={mockedApolloClient}>
<ApolloCoreClientMockedProvider> <ApolloCoreClientMockedProvider>
{!disableHotkeyInitialization && (
<InitializeHotkeyStorybookHookEffect />
)}
<Story /> <Story />
</ApolloCoreClientMockedProvider> </ApolloCoreClientMockedProvider>
</ApolloProvider> </ApolloProvider>