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 { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { v4 } from 'uuid';
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 { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
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 { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
@ -24,9 +23,6 @@ import { Task } from '@/activities/types/Task';
import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore';
import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete';
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 { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
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 { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import type { PartialBlock } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
@ -304,24 +301,17 @@ export const ActivityRichTextEditor = ({
uploadFile: handleEditorBuiltInUploadFile,
});
const commandMenuPage = useRecoilValue(commandMenuPageState);
useScopedHotkeys(
Key.Escape,
() => {
useHotkeysOnFocusedElement({
keys: Key.Escape,
callback: () => {
editor.domElement?.blur();
},
ActivityEditorHotkeyScope.ActivityBody,
);
useScopedHotkeys(
'*',
(keyboardEvent) => {
// TODO: remove once stacked hotkeys / focusKeys are in place
if (commandMenuPage !== CommandMenuPages.EditRichText) {
return;
}
focusId: activityId,
scope: ActivityEditorHotkeyScope.ActivityBody,
dependencies: [editor],
});
const handleAllKeys = (keyboardEvent: KeyboardEvent) => {
if (keyboardEvent.key === Key.Escape) {
return;
}
@ -351,13 +341,16 @@ export const ActivityRichTextEditor = ({
editor.setTextCursorPosition(newBlockId, 'end');
editor.focus();
},
CommandMenuHotkeyScope.CommandMenuFocused,
[],
{
preventDefault: false,
},
);
};
useHotkeysOnFocusedElement({
keys: '*',
callback: handleAllKeys,
focusId: activityId,
scope: ActivityEditorHotkeyScope.ActivityBody,
dependencies: [handleAllKeys],
});
const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({
objectNameSingular: activityObjectNameSingular,
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 { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath';
import { PageFocusId } from '@/types/PageFocusId';
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 { isDefined } from 'twenty-shared/utils';
import { AnalyticsType } from '~/generated/graphql';
@ -48,8 +50,6 @@ export const PageChangeEffect = () => {
const [previousLocation, setPreviousLocation] = useState('');
const setHotkeyScope = useSetHotkeyScope();
const location = useLocation();
const pageChangeEffectNavigateLocation =
@ -92,6 +92,8 @@ export const PageChangeEffect = () => {
const { closeCommandMenu } = useCommandMenu();
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex();
useEffect(() => {
@ -140,55 +142,200 @@ export const PageChangeEffect = () => {
break;
}
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,
keyboardShortcutMenu: true,
searchRecords: true,
},
},
});
break;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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,
keyboardShortcutMenu: false,
commandMenu: false,
commandMenuOpen: false,
searchRecords: false,
},
},
});
break;
}
}
}, [
location,
setHotkeyScope,
previousLocation,
contextStoreCurrentViewType,
resetTableSelections,
@ -198,6 +345,7 @@ export const PageChangeEffect = () => {
deactivateBoardCard,
unfocusBoardCard,
resetFocusStackToRecordIndex,
resetFocusStackToFocusItem,
]);
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 { 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 { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
@ -15,15 +18,15 @@ export const KeyboardShortcutMenuOpenContent = () => {
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
useKeyboardShortcutMenu();
useGlobalHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
closeKeyboardShortcutMenu();
},
false,
AppHotkeyScope.KeyboardShortcutMenuOpen,
[closeKeyboardShortcutMenu],
);
focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
scope: AppHotkeyScope.KeyboardShortcutMenuOpen,
dependencies: [closeKeyboardShortcutMenu],
});
return (
<>

View File

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

View File

@ -1,25 +1,39 @@
import { useRecoilCallback } from 'recoil';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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';
export const KEYBOARD_SHORTCUT_MENU_INSTANCE_ID = 'keyboard-shortcut-menu';
export const useKeyboardShortcutMenu = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const openKeyboardShortcutMenu = useRecoilCallback(
({ set }) =>
() => {
set(isKeyboardShortcutMenuOpenedState, true);
setHotkeyScopeAndMemorizePreviousScope({
scope: AppHotkeyScope.KeyboardShortcutMenu,
pushFocusItemToFocusStack({
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(
@ -31,10 +45,12 @@ export const useKeyboardShortcutMenu = () => {
if (isKeyboardShortcutMenuOpened) {
set(isKeyboardShortcutMenuOpenedState, false);
goBackToPreviousHotkeyScope();
removeFocusItemFromFocusStackById({
focusId: KEYBOARD_SHORTCUT_MENU_INSTANCE_ID,
});
}
},
[goBackToPreviousHotkeyScope],
[removeFocusItemFromFocusStackById],
);
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 { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation';
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 { PageFocusId } from '@/types/PageFocusId';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
export const RecordBoardHotkeyEffect = () => {
@ -18,7 +18,7 @@ export const RecordBoardHotkeyEffect = () => {
callback: () => {
move('down');
},
focusId: RECORD_INDEX_FOCUS_ID,
focusId: PageFocusId.RecordIndex,
scope: RecordIndexHotkeyScope.RecordIndex,
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 { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
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 { MenuItem } from 'twenty-ui/navigation';
const StyledMenuContainer = styled.div`
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);
export const RecordBoardColumnDropdownMenu = () => {
const recordGroupActions = useRecordGroupActions({
viewType: ViewType.Kanban,
});
const closeMenu = useCallback(() => {
onClose();
}, [onClose]);
useListenClickOutside({
refs: [boardColumnMenuRef],
callback: closeMenu,
listenerId: 'record-board-column-dropdown-menu',
});
return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<OverlayContainer>
<DropdownContent selectDisabled>
<DropdownMenuItemsContainer>
{recordGroupActions.map((action) => (
@ -51,7 +17,6 @@ export const RecordBoardColumnDropdownMenu = ({
key={action.id}
onClick={() => {
action.callback();
closeMenu();
}}
LeftIcon={action.icon}
text={action.label}
@ -59,7 +24,5 @@ export const RecordBoardColumnDropdownMenu = ({
))}
</DropdownMenuItemsContainer>
</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 { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
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 { 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 { IconDotsVertical, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
@ -66,32 +66,11 @@ const StyledTag = styled(Tag)`
export const RecordBoardColumnHeader = () => {
const { columnDefinition } = useContext(RecordBoardColumnContext);
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem, selectFieldMetadataItem } =
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 } =
useAggregateRecordsForRecordBoardColumn();
@ -105,6 +84,10 @@ export const RecordBoardColumnHeader = () => {
objectMetadataItem: objectMetadataItem,
});
const { toggleDropdown } = useToggleDropdown();
const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`;
return (
<StyledColumn>
<StyledHeader
@ -113,8 +96,15 @@ export const RecordBoardColumnHeader = () => {
>
<StyledHeaderContainer>
<StyledLeftContainer>
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="bottom-start"
dropdownOffset={{
x: 0,
y: 10,
}}
clickableComponent={
<StyledTag
onClick={handleBoardColumnMenuOpen}
variant={
columnDefinition.type === RecordGroupDefinitionType.Value
? 'solid'
@ -132,6 +122,10 @@ export const RecordBoardColumnHeader = () => {
: 'medium'
}
/>
}
dropdownComponents={<RecordBoardColumnDropdownMenu />}
/>
<RecordBoardColumnHeaderAggregateDropdown
aggregateValue={aggregateValue}
dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`}
@ -145,7 +139,11 @@ export const RecordBoardColumnHeader = () => {
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
onClick={() => {
toggleDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
}}
/>
{hasObjectUpdatePermissions && (
<LightIconButton
@ -164,12 +162,6 @@ export const RecordBoardColumnHeader = () => {
</StyledRightContainer>
</StyledHeaderContainer>
</StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
stageId={columnDefinition.id}
/>
)}
</StyledColumn>
);
};

View File

@ -1,36 +1,24 @@
import { Key } from 'ts-key-enum';
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import {
RecordBoardColumnHeaderAggregateDropdownContext,
RecordBoardColumnHeaderAggregateDropdownContextValue,
} 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useLingui } from '@lingui/react/macro';
import { MenuItem } from 'twenty-ui/navigation';
export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
const { t } = useLingui();
const { onContentChange, closeDropdown } =
const { onContentChange } =
useDropdownContextStateManagement<RecordBoardColumnHeaderAggregateDropdownContextValue>(
{
context: RecordBoardColumnHeaderAggregateDropdownContext,
},
);
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
return (
<DropdownContent>
<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 { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
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 { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
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 { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate';
import isEmpty from 'lodash.isempty';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconCheck, IconChevronLeft } from 'twenty-ui/display';
export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
@ -38,14 +35,6 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
},
);
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const setAggregateOperation = useSetRecoilComponentStateV2(
aggregateOperationComponentState,
);

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 styled from '@emotion/styled';
import { forwardRef, HTMLAttributes, Ref } from 'react';
@ -9,9 +11,12 @@ type FormFieldInputInnerContainerProps = {
multiline?: boolean;
readonly?: boolean;
preventSetHotkeyScope?: boolean;
formFieldInputInstanceId: string;
};
const StyledFormFieldInputInnerContainer = styled.div<FormFieldInputInnerContainerProps>`
const StyledFormFieldInputInnerContainer = styled.div<
Omit<FormFieldInputInnerContainerProps, 'formFieldInputInstanceId'>
>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
@ -48,20 +53,25 @@ export const FormFieldInputInnerContainer = forwardRef(
readonly,
preventSetHotkeyScope = false,
onClick,
formFieldInputInstanceId,
}: HTMLAttributes<HTMLDivElement> & FormFieldInputInnerContainerProps,
ref: Ref<HTMLDivElement>,
) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
onFocus?.(e);
if (!preventSetHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope({
scope: FormFieldInputHotKeyScope.FormFieldInput,
pushFocusItemToFocusStack({
focusId: formFieldInputInstanceId,
component: {
type: FocusComponentType.FORM_FIELD_INPUT,
instanceId: formFieldInputInstanceId,
},
hotkeyScope: { scope: FormFieldInputHotKeyScope.FormFieldInput },
});
}
};
@ -70,7 +80,9 @@ export const FormFieldInputInnerContainer = forwardRef(
onBlur?.(e);
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
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 { useTheme } from '@emotion/react';
import { isArray } from '@sniptt/guards';
@ -88,10 +90,9 @@ export const FormMultiSelectFieldInput = ({
const hotkeyScope =
FormMultiSelectFieldInputHotKeyScope.FormMultiSelectFieldInput;
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const [draftValue, setDraftValue] = useState<
| {
@ -128,8 +129,13 @@ export const FormMultiSelectFieldInput = ({
editingMode: 'edit',
});
setHotkeyScopeAndMemorizePreviousScope({
scope: hotkeyScope,
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.FORM_FIELD_INPUT,
instanceId,
},
hotkeyScope: { scope: hotkeyScope },
});
};
@ -157,7 +163,7 @@ export const FormMultiSelectFieldInput = ({
editingMode: 'view',
});
goBackToPreviousHotkeyScope();
removeFocusItemFromFocusStackById({ focusId: instanceId });
};
const handleVariableTagInsert = (variableName: string) => {
@ -201,6 +207,7 @@ export const FormMultiSelectFieldInput = ({
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (

View File

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

View File

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

View File

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

View File

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

View File

@ -95,6 +95,7 @@ export const FormUuidFieldInput = ({
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{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 { PageFocusId } from '@/types/PageFocusId';
import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -9,10 +9,10 @@ export const useResetFocusStackToRecordIndex = () => {
const resetFocusStackToRecordIndex = () => {
resetFocusStackToFocusItem({
focusStackItem: {
focusId: RECORD_INDEX_FOCUS_ID,
focusId: PageFocusId.RecordIndex,
componentInstance: {
componentType: FocusComponentType.PAGE,
componentInstanceId: RECORD_INDEX_FOCUS_ID,
componentInstanceId: PageFocusId.RecordIndex,
},
globalHotkeysConfig: {
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 { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
@ -48,16 +47,6 @@ export const RecordTableWithWrappers = ({
selectAllRows();
};
useScopedHotkeys(
'ctrl+a,meta+a',
handleSelectAllRows,
RecordIndexHotkeyScope.RecordIndex,
[],
{
enableOnFormTags: false,
},
);
useHotkeysOnFocusedElement({
keys: ['ctrl+a,meta+a'],
callback: handleSelectAllRows,

View File

@ -1,10 +1,10 @@
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 { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -28,7 +28,7 @@ export const RecordTableBodyEscapeHotkeyEffect = () => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handleEscape,
focusId: RECORD_INDEX_FOCUS_ID,
focusId: PageFocusId.RecordIndex,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [handleEscape],
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 { useRecordTableRowFocusHotkeys } from '@/object-record/record-table/hooks/useRecordTableRowFocusHotkeys';
import { PageFocusId } from '@/types/PageFocusId';
export const RecordTableBodyFocusKeyboardEffect = () => {
useRecordTableRowFocusHotkeys({
focusId: RECORD_INDEX_FOCUS_ID,
focusId: PageFocusId.RecordIndex,
hotkeyScope: RecordIndexHotkeyScope.RecordIndex,
});

View File

@ -1,15 +1,11 @@
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 { 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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
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 { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui/display';
export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
@ -19,19 +15,10 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
aggregateOperations: ExtendedAggregateOperations[];
title: string;
}) => {
const { dropdownId, resetContent } = useContext(
const { resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useCloseDropdown();
useScopedHotkeys(
[Key.Escape],
() => {
resetContent();
closeDropdown(dropdownId);
},
TableOptionsHotkeyScope.Dropdown,
);
return (
<DropdownContent>
<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 { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { t } from '@lingui/core/macro';
import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
@ -28,14 +25,6 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
const { closeDropdown } = useCloseDropdown();
const { objectMetadataItem } = useRecordTableContextOrThrow();
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown(dropdownId);
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperation = useMemo(
() =>
getAvailableAggregateOperationsForFieldMetadataType({

View File

@ -3,14 +3,10 @@ import {
SettingsServerlessFunctionCodeEditor,
} from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor';
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 { 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 styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import {
H2Title,
IconGitCommit,
@ -19,8 +15,6 @@ import {
} from 'twenty-ui/display';
import { Button, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledTabList = styled(TabList)`
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 (
<Section>
<H2Title

View File

@ -2,16 +2,12 @@ import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functio
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
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 { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal';
@ -36,25 +32,6 @@ export const SettingsServerlessFunctionSettingsTab = ({
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 (
<>
<SettingsServerlessFunctionNewForm

View File

@ -1,15 +1,9 @@
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 styled from '@emotion/styled';
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 { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
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 (
<Section>
<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 { 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_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId';
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 { useRef } from 'react';
import { isDefined } from 'twenty-shared/utils';
@ -96,9 +96,7 @@ export const Dialog = ({
closed: { y: '50vh' },
};
useScopedHotkeys(
Key.Enter,
(event: KeyboardEvent) => {
const handleEnter = (event: KeyboardEvent) => {
const confirmButton = buttons.find((button) => button.role === 'confirm');
event.preventDefault();
@ -107,20 +105,28 @@ export const Dialog = ({
confirmButton?.onClick?.(event);
onClose?.();
}
},
DialogHotkeyScope.Dialog,
[],
);
};
useScopedHotkeys(
Key.Escape,
(event: KeyboardEvent) => {
const handleEscape = (event: KeyboardEvent) => {
event.preventDefault();
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);

View File

@ -1,26 +1,34 @@
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 { 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 { DialogHotkeyScope } from '../types/DialogHotkeyScope';
export const DialogManagerEffect = () => {
const { dialogInternal } = useDialogManagerScopedStates();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
useEffect(() => {
if (dialogInternal.queue.length === 0) {
return;
}
setHotkeyScopeAndMemorizePreviousScope({
pushFocusItemToFocusStack({
focusId: DIALOG_FOCUS_ID,
component: {
type: FocusComponentType.DIALOG,
instanceId: DIALOG_FOCUS_ID,
},
hotkeyScope: {
scope: DialogHotkeyScope.Dialog,
},
memoizeKey: DIALOG_MANAGER_HOTKEY_SCOPE_MEMOIZE_KEY,
});
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]);
}, [dialogInternal.queue, pushFocusItemToFocusStack]);
return <></>;
};

View File

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

View File

@ -1,10 +1,10 @@
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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 { dialogInternalScopedState } from '../states/dialogInternalScopedState';
import { DialogOptions } from '../types/DialogOptions';
@ -19,7 +19,8 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
props?.dialogManagerScopeId,
);
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeDialog = useRecoilCallback(
({ set }) =>
@ -29,9 +30,9 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
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(

View File

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

View File

@ -9,6 +9,9 @@ export enum FocusComponentType {
RECORD_TABLE_CELL = 'record-table-cell',
TEXT_AREA = 'text-area',
TEXT_INPUT = 'text-input',
FORM_FIELD_INPUT = 'form-field-input',
RECORD_BOARD_CARD = 'record-board-card',
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useCallback, useState } from 'react';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
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 { v4 } from 'uuid';
@ -117,17 +117,21 @@ export const useAgentChat = (agentId: string) => {
await sendChatMessage(content);
};
useScopedHotkeys(
[Key.Enter],
(event) => {
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: (event: KeyboardEvent) => {
if (!event.ctrlKey && !event.metaKey) {
event.preventDefault();
handleSendMessage();
}
},
InputHotkeyScope.TextInput,
[agentChatInput, isLoading],
);
focusId: `${agentId}-chat-input`,
scope: InputHotkeyScope.TextInput,
dependencies: [agentChatInput, isLoading],
options: {
enableOnFormTags: true,
},
});
return {
handleInputChange: (value: string) => setAgentChatInput(value),

View File

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

View File

@ -47,7 +47,10 @@ export const WorkflowFormEmptyMessage = () => {
<StyledMessageContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer multiline maxHeight={124}>
<FormFieldInputInnerContainer hasRightElement={false}>
<FormFieldInputInnerContainer
formFieldInputInstanceId="empty-form-message"
hasRightElement={false}
>
<StyledFieldContainer>
<StyledMessageContentContainer>
<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 { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
import { PageFocusId } from '@/types/PageFocusId';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
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 { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro';
@ -148,15 +149,23 @@ export const CreateProfile = () => {
const [isEditingMode, setIsEditingMode] = useState(false);
useScopedHotkeys(
Key.Enter,
() => {
const handleEnter = () => {
if (isEditingMode) {
onSubmit(getValues());
}
};
useHotkeysOnFocusedElement({
keys: Key.Enter,
callback: () => {
if (isEditingMode) {
onSubmit(getValues());
}
},
PageHotkeyScope.CreateProfile,
);
focusId: PageFocusId.CreateProfile,
scope: PageHotkeyScope.CreateProfile,
dependencies: [handleEnter],
});
return (
<Modal.Content isVerticalCentered isHorizontalCentered>

View File

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

View File

@ -9,7 +9,6 @@ import { Title } from '@/auth/components/Title';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
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 { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { AppPath } from '@/types/AppPath';
import { PageFocusId } from '@/types/PageFocusId';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { IconGoogle, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
@ -95,14 +96,15 @@ export const SyncEmails = () => {
const isMicrosoftProviderEnabled =
isMicrosoftMessagingEnabled || isMicrosoftCalendarEnabled;
useScopedHotkeys(
[Key.Enter],
async () => {
useHotkeysOnFocusedElement({
keys: Key.Enter,
callback: async () => {
await continueWithoutSync();
},
PageHotkeyScope.SyncEmail,
[continueWithoutSync],
);
focusId: PageFocusId.SyncEmail,
scope: PageHotkeyScope.SyncEmail,
dependencies: [continueWithoutSync],
});
return (
<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 { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { isDefined } from 'twenty-shared/utils';
export const SettingsServerlessFunctionsNew = () => {
const navigate = useNavigateSettings();
@ -50,28 +46,6 @@ export const SettingsServerlessFunctionsNew = () => {
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 (
<SubMenuTopBarContainer
title="New Function"

View File

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