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:
@ -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,
|
||||
]);
|
||||
};
|
||||
@ -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,60 +301,56 @@ export const ActivityRichTextEditor = ({
|
||||
uploadFile: handleEditorBuiltInUploadFile,
|
||||
});
|
||||
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.Escape,
|
||||
callback: () => {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyboardEvent.key === Key.Escape) {
|
||||
return;
|
||||
}
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopPropagation();
|
||||
keyboardEvent.stopImmediatePropagation();
|
||||
const newBlockId = v4();
|
||||
const newBlock = {
|
||||
id: newBlockId,
|
||||
type: 'paragraph' as const,
|
||||
content: keyboardEvent.key,
|
||||
};
|
||||
|
||||
const newBlockId = v4();
|
||||
const newBlock = {
|
||||
id: newBlockId,
|
||||
type: 'paragraph' as const,
|
||||
content: keyboardEvent.key,
|
||||
};
|
||||
const lastBlock = editor.document[editor.document.length - 1];
|
||||
editor.insertBlocks([newBlock], lastBlock);
|
||||
|
||||
const lastBlock = editor.document[editor.document.length - 1];
|
||||
editor.insertBlocks([newBlock], lastBlock);
|
||||
editor.setTextCursorPosition(newBlockId, 'end');
|
||||
editor.focus();
|
||||
};
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: '*',
|
||||
callback: handleAllKeys,
|
||||
focusId: activityId,
|
||||
scope: ActivityEditorHotkeyScope.ActivityBody,
|
||||
dependencies: [handleAllKeys],
|
||||
});
|
||||
|
||||
editor.setTextCursorPosition(newBlockId, 'end');
|
||||
editor.focus();
|
||||
},
|
||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
||||
[],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
objectRecordId: activityId,
|
||||
|
||||
@ -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, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
searchRecords: true,
|
||||
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, {
|
||||
goto: false,
|
||||
keyboardShortcutMenu: false,
|
||||
commandMenu: false,
|
||||
commandMenuOpen: false,
|
||||
searchRecords: false,
|
||||
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(() => {
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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],
|
||||
});
|
||||
|
||||
@ -1,65 +1,28 @@
|
||||
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) => (
|
||||
<MenuItem
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
action.callback();
|
||||
closeMenu();
|
||||
}}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
</OverlayContainer>
|
||||
</StyledMenuContainer>
|
||||
<DropdownContent selectDisabled>
|
||||
<DropdownMenuItemsContainer>
|
||||
{recordGroupActions.map((action) => (
|
||||
<MenuItem
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
action.callback();
|
||||
}}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,25 +96,36 @@ export const RecordBoardColumnHeader = () => {
|
||||
>
|
||||
<StyledHeaderContainer>
|
||||
<StyledLeftContainer>
|
||||
<StyledTag
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
variant={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? columnDefinition.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={columnDefinition.title}
|
||||
weight={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownOffset={{
|
||||
x: 0,
|
||||
y: 10,
|
||||
}}
|
||||
clickableComponent={
|
||||
<StyledTag
|
||||
variant={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? columnDefinition.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={columnDefinition.title}
|
||||
weight={
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'regular'
|
||||
: '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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -85,6 +85,7 @@ export const FormBooleanFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
>
|
||||
{draftValue.type === 'static' ? (
|
||||
|
||||
@ -296,6 +296,7 @@ export const FormDateTimeFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<StyledInputContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
ref={datePickerWrapperRef}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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' ? (
|
||||
|
||||
@ -121,6 +121,7 @@ export const FormNumberFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
|
||||
@ -71,6 +71,7 @@ export const FormRawJsonFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer multiline>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
multiline
|
||||
onBlur={onBlur}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -71,6 +71,7 @@ export const FormTextFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer multiline={multiline}>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
multiline={multiline}
|
||||
onBlur={onBlur}
|
||||
|
||||
@ -95,6 +95,7 @@ export const FormUuidFieldInput = ({
|
||||
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId={instanceId}
|
||||
hasRightElement={isDefined(VariablePicker) && !readonly}
|
||||
>
|
||||
{draftValue.type === 'static' ? (
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export const RECORD_INDEX_FOCUS_ID = 'record-index';
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
11
packages/twenty-front/src/modules/types/PageFocusId.ts
Normal file
11
packages/twenty-front/src/modules/types/PageFocusId.ts
Normal 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',
|
||||
}
|
||||
@ -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,31 +96,37 @@ export const Dialog = ({
|
||||
closed: { y: '50vh' },
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
(event: KeyboardEvent) => {
|
||||
const confirmButton = buttons.find((button) => button.role === 'confirm');
|
||||
const handleEnter = (event: KeyboardEvent) => {
|
||||
const confirmButton = buttons.find((button) => button.role === 'confirm');
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
if (isDefined(confirmButton)) {
|
||||
confirmButton?.onClick?.(event);
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
DialogHotkeyScope.Dialog,
|
||||
[],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
(event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
if (isDefined(confirmButton)) {
|
||||
confirmButton?.onClick?.(event);
|
||||
onClose?.();
|
||||
},
|
||||
DialogHotkeyScope.Dialog,
|
||||
[],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@ -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({
|
||||
scope: DialogHotkeyScope.Dialog,
|
||||
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 <></>;
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const DIALOG_FOCUS_ID = 'dialog';
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
export const DEBUG_HOTKEY_SCOPE = true;
|
||||
export const DEBUG_HOTKEY_SCOPE = false;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
};
|
||||
@ -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),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user