From 4d352cb4e48c1549e22fbcdd0543cdb74ab3049e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 12 May 2025 19:02:14 +0200 Subject: [PATCH] Keyboard Navigation and Shortcuts Implementation on the board (#11930) # Keyboard Navigation and Shortcuts Implementation on the board This PR implements keyboard navigation and shortcuts for the Record Board component, enabling users to navigate and interact with board cards using keyboard inputs. ## Key changes ### Navigation Architecture - Added `useBoardCardNavigation` hook for directional navigation (arrow keys) - Implemented scroll behavior to automatically focus visible cards - Created card focus/active states with component-level management ### State Management - Added new component states: `focusedBoardCardIndexesComponentState`, `activeBoardCardIndexesComponentState`, `isBoardCardActiveComponentFamilyState` - Extended index tracking with column/row position indicators - Create hooks to manage these states ### Hotkey Implementation - Created new `RecordBoardHotkeyEffect` component for hotkey handling - Added `BoardHotkeyScope` - Implemented Escape key handling to clear selections - Replaced table-specific `TableHotkeyScope` with more generic `RecordIndexHotkeyScope`. This is because, before, the hotkey scope was always set to Table inside the page change effect, and we used the table hotkey scope for board shortcuts, which doesn't make a lot of sense. Since we don't know upon navigation on which type of view we are navigating, I introduced this generic hotkey scope which can be used on the table and on the board. ### Page Navigation Integration - Modified `PageChangeEffect` to handle both table and board view types - Added cleanup for board state upon navigating away from record pages ### Component Updates - Updated `RecordBoardColumn` to track indexes for position-based navigation - Added `RecordBoardScrollToFocusedCardEffect` for auto-scrolling to focused cards - Added `RecordBoardDeactivateBoardCardEffect` for cleanup This implementation maintains feature parity with table row navigation while accounting for the 2D navigation needs of the board view. ## New behaviors ### Arrow keys navigation https://github.com/user-attachments/assets/929ee00d-2f82-43b9-8cde-f7bc8818052f ### Record selection with X https://github.com/user-attachments/assets/0b534c4d-2865-43ac-8ba3-09cb8c121f06 ### Command + Enter opens the record https://github.com/user-attachments/assets/0df01d1c-0437-4444-beb1-ce74bcfb91a4 ### Escape unselect the records and unfocus the card https://github.com/user-attachments/assets/e2bb176b-b6f7-49ca-9549-803eb31bfc23 --- .../actions/components/ActionModal.tsx | 3 +- .../effect-components/PageChangeEffect.tsx | 35 ++- .../hooks/useNavigateCommandMenu.ts | 8 +- .../useKeyboardShortcutMenu.test.tsx | 3 +- .../hooks/useKeyboardShortcutMenu.ts | 66 +++--- .../record-board/components/RecordBoard.tsx | 38 ++- .../RecordBoardBodyEscapeHotkeyEffect.tsx | 52 +++++ .../components/RecordBoardHeader.tsx | 3 +- .../components/RecordBoardHotkeyEffect.tsx | 121 ++++++++++ .../RecordBoardScrollToFocusedCardEffect.tsx | 45 ++++ .../hooks/useActiveRecordBoardCard.ts | 64 ++++++ .../hooks/useFocusedRecordBoardCard.ts | 70 ++++++ .../hooks/useRecordBoardCardNavigation.ts | 216 ++++++++++++++++++ .../hooks/useRecordBoardSelection.ts | 11 +- .../components/RecordBoardCard.tsx | 72 ++++-- .../RecordBoardCardDraggableContainer.tsx | 40 +++- .../RecordBoardCardFocusHotkeyEffect.tsx | 57 +++++ .../RecordBoardDeactivateBoardCardEffect.tsx | 15 ++ .../contexts/RecordBoardCardContext.ts | 2 + .../components/RecordBoardColumn.tsx | 3 + .../components/RecordBoardColumnCardsMemo.tsx | 2 +- .../RecordBoardColumnHeaderWrapper.tsx | 3 + .../contexts/RecordBoardColumnContext.ts | 1 + ...iveRecordBoardCardIndexesComponentState.ts | 10 + ...sedRecordBoardCardIndexesComponentState.ts | 10 + ...cordBoardCardActiveComponentFamilyState.ts | 10 + ...ecordBoardCardFocusActiveComponentState.ts | 9 + ...ordBoardCardFocusedComponentFamilyState.ts | 10 + .../record-board/types/BoardCardIndexes.ts | 4 + .../record-board/types/BoardHotkeyScope.ts | 3 + .../components/RecordIndexBoardContainer.tsx | 5 +- .../types/RecordIndexHotkeyScope.ts | 3 + .../components/RecordTableWithWrappers.tsx | 13 +- .../hooks/internal/useLeaveTableFocus.ts | 4 +- .../record-table/hooks/useRecordTable.ts | 5 +- .../RecordTableBodyEscapeHotkeyEffect.tsx | 7 +- .../RecordTableBodyFocusKeyboardEffect.tsx | 3 +- .../hooks/useMoveHoverToCurrentCell.ts | 5 +- .../components/RecordTableTr.tsx | 10 +- .../record-table/types/TableHotkeyScope.ts | 1 - 40 files changed, 933 insertions(+), 109 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useActiveRecordBoardCard.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardNavigation.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/activeRecordBoardCardIndexesComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/types/BoardCardIndexes.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/types/BoardHotkeyScope.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/types/RecordIndexHotkeyScope.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx index 14787efc4e5..c21113deac2 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx @@ -33,9 +33,8 @@ export const ActionModal = ({ const { closeActionMenu } = useCloseActionMenu(); const handleConfirmClick = () => { - closeActionMenu(); onConfirmClick(); - setIsOpen(false); + closeActionMenu(); }; const actionConfig = useContext(ActionConfigContext); diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index d9971259062..0749116df27 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -17,11 +17,16 @@ import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoad import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; +import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; +import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; +import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; -import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { AppBasePath } from '@/types/AppBasePath'; import { AppPath } from '@/types/AppPath'; @@ -64,6 +69,11 @@ export const PageChangeEffect = () => { MAIN_CONTEXT_STORE_INSTANCE_ID, ); + const contextStoreCurrentViewType = useRecoilComponentValueV2( + contextStoreCurrentViewTypeComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId( objectNamePlural, contextStoreCurrentViewId || '', @@ -73,6 +83,10 @@ export const PageChangeEffect = () => { const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId); const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId); + const { resetRecordSelection } = useRecordBoardSelection(recordIndexId); + const { deactivateBoardCard } = useActiveRecordBoardCard(recordIndexId); + const { unfocusBoardCard } = useFocusedRecordBoardCard(recordIndexId); + const { executeTasksOnAnyLocationChange } = useExecuteTasksOnAnyLocationChange(); @@ -100,9 +114,16 @@ export const PageChangeEffect = () => { ); if (isLeavingRecordIndexPage) { - resetTableSelections(); - unfocusRecordTableRow(); - deactivateRecordTableRow(); + if (contextStoreCurrentViewType === ContextStoreViewType.Table) { + resetTableSelections(); + unfocusRecordTableRow(); + deactivateRecordTableRow(); + } + if (contextStoreCurrentViewType === ContextStoreViewType.Kanban) { + resetRecordSelection(); + deactivateBoardCard(); + unfocusBoardCard(); + } } }, [ isMatchingLocation, @@ -110,12 +131,16 @@ export const PageChangeEffect = () => { resetTableSelections, unfocusRecordTableRow, deactivateRecordTableRow, + contextStoreCurrentViewType, + resetRecordSelection, + deactivateBoardCard, + unfocusBoardCard, ]); useEffect(() => { switch (true) { case isMatchingLocation(AppPath.RecordIndexPage): { - setHotkeyScope(TableHotkeyScope.Table, { + setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, { goto: true, keyboardShortcutMenu: true, }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts index 36bb69231c8..c92fb987db9 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts @@ -51,6 +51,10 @@ export const useNavigateCommandMenu = () => { commandMenuCloseAnimationCompleteCleanup(); } + if (isCommandMenuOpened) { + return; + } + setHotkeyScopeAndMemorizePreviousScope( CommandMenuHotkeyScope.CommandMenuFocused, { @@ -58,10 +62,6 @@ export const useNavigateCommandMenu = () => { }, ); - if (isCommandMenuOpened) { - return; - } - copyContextStoreStates({ instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID, instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID, diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx index 49d98de1547..d38b3b57748 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/__tests__/useKeyboardShortcutMenu.test.tsx @@ -1,5 +1,6 @@ import { expect } from '@storybook/test'; -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; import { RecoilRoot, useRecoilValue } from 'recoil'; import { isKeyboardShortcutMenuOpenedState } from '@/keyboard-shortcut-menu/states/isKeyboardShortcutMenuOpenedState'; diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts index 347e40db0ba..0d568a47353 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu.ts @@ -1,4 +1,4 @@ -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; @@ -6,38 +6,52 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState'; export const useKeyboardShortcutMenu = () => { - const [, setIsKeyboardShortcutMenuOpened] = useRecoilState( - isKeyboardShortcutMenuOpenedState, - ); - const isKeyboardShortcutMenuOpened = useRecoilValue( - isKeyboardShortcutMenuOpenedState, - ); const { setHotkeyScopeAndMemorizePreviousScope, goBackToPreviousHotkeyScope, } = usePreviousHotkeyScope(); - const toggleKeyboardShortcutMenu = () => { - if (isKeyboardShortcutMenuOpened === false) { - setIsKeyboardShortcutMenuOpened(true); - setHotkeyScopeAndMemorizePreviousScope( - AppHotkeyScope.KeyboardShortcutMenu, - ); - } else { - setIsKeyboardShortcutMenuOpened(false); - goBackToPreviousHotkeyScope(); - } - }; + const openKeyboardShortcutMenu = useRecoilCallback( + ({ set }) => + () => { + set(isKeyboardShortcutMenuOpenedState, true); + setHotkeyScopeAndMemorizePreviousScope( + AppHotkeyScope.KeyboardShortcutMenu, + ); + }, + [setHotkeyScopeAndMemorizePreviousScope], + ); - const openKeyboardShortcutMenu = () => { - setIsKeyboardShortcutMenuOpened(true); - setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.KeyboardShortcutMenu); - }; + const closeKeyboardShortcutMenu = useRecoilCallback( + ({ set, snapshot }) => + () => { + const isKeyboardShortcutMenuOpened = snapshot + .getLoadable(isKeyboardShortcutMenuOpenedState) + .getValue(); - const closeKeyboardShortcutMenu = () => { - setIsKeyboardShortcutMenuOpened(false); - goBackToPreviousHotkeyScope(); - }; + if (isKeyboardShortcutMenuOpened) { + set(isKeyboardShortcutMenuOpenedState, false); + goBackToPreviousHotkeyScope(); + } + }, + [goBackToPreviousHotkeyScope], + ); + + const toggleKeyboardShortcutMenu = useRecoilCallback( + ({ snapshot }) => + () => { + const isKeyboardShortcutMenuOpened = snapshot + .getLoadable(isKeyboardShortcutMenuOpenedState) + .getValue(); + + if (isKeyboardShortcutMenuOpened === false) { + openKeyboardShortcutMenu(); + } else { + closeKeyboardShortcutMenu(); + } + }, + [closeKeyboardShortcutMenu, openKeyboardShortcutMenu], + ); return { toggleKeyboardShortcutMenu, diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index c87033b9b6b..04b57f57faf 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -5,10 +5,14 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; +import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect'; import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; +import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect'; import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn'; import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope'; import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; @@ -16,14 +20,11 @@ import { getDraggedRecordPosition } from '@/object-record/record-board/utils/get import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; -import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; -import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; @@ -71,6 +72,9 @@ export const RecordBoard = () => { const { closeDropdown } = useDropdownV2(); + const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId); + const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); + const handleDragSelectionStart = () => { closeDropdown(actionMenuId); @@ -91,10 +95,6 @@ export const RecordBoard = () => { recordIndexRecordIdsByGroupComponentFamilyState, ); - const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2( - recordIndexAllRecordIdsComponentSelector, - ); - const { resetRecordSelection, setRecordAsSelected } = useRecordBoardSelection(recordBoardId); @@ -115,26 +115,11 @@ export const RecordBoard = () => { refs: [], callback: () => { resetRecordSelection(); + deactivateBoardCard(); + unfocusBoardCard(); }, }); - const selectAll = useRecoilCallback( - ({ snapshot }) => - () => { - const allRecordIds = getSnapshotValue( - snapshot, - recordIndexAllRecordIdsState, - ); - - for (const recordId of allRecordIds) { - setRecordAsSelected(recordId, true); - } - }, - [recordIndexAllRecordIdsState, setRecordAsSelected], - ); - - useScopedHotkeys('ctrl+a,meta+a', selectAll, TableHotkeyScope.Table); - const setIsRemoveSortingModalOpen = useSetRecoilState( isRemoveSortingModalOpenState, ); @@ -228,16 +213,19 @@ export const RecordBoard = () => { componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`} > + + - {visibleRecordGroupIds.map((recordGroupId) => ( + {visibleRecordGroupIds.map((recordGroupId, index) => ( ))} diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx new file mode 100644 index 00000000000..717aaf11f33 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx @@ -0,0 +1,52 @@ +import { useContext } from 'react'; +import { Key } from 'ts-key-enum'; + +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; +import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; +import { BoardHotkeyScope } from '@/object-record/record-board/types/BoardHotkeyScope'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordBoardBodyEscapeHotkeyEffect = () => { + const { recordBoardId } = useContext(RecordBoardContext); + + const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); + + const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); + + const selectedRecordIds = useRecoilComponentValueV2( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, + ); + + const isAtLeastOneRecordSelected = selectedRecordIds.length > 0; + + useScopedHotkeys( + [Key.Escape], + () => { + unfocusBoardCard(); + if (isAtLeastOneRecordSelected) { + resetRecordSelection(); + } + }, + RecordIndexHotkeyScope.RecordIndex, + [isAtLeastOneRecordSelected, resetRecordSelection, unfocusBoardCard], + ); + + useScopedHotkeys( + [Key.Escape], + () => { + unfocusBoardCard(); + if (isAtLeastOneRecordSelected) { + resetRecordSelection(); + } + }, + BoardHotkeyScope.BoardFocus, + [isAtLeastOneRecordSelected, resetRecordSelection, unfocusBoardCard], + ); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index 3ca0b6e680e..187606a64f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -31,9 +31,10 @@ export const RecordBoardHeader = () => { return ( - {visibleRecordGroupIds.map((recordGroupId) => ( + {visibleRecordGroupIds.map((recordGroupId, index) => ( ))} diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx new file mode 100644 index 00000000000..6ceebcdc10f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx @@ -0,0 +1,121 @@ +import { useContext } from 'react'; +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 { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { BoardHotkeyScope } from '@/object-record/record-board/types/BoardHotkeyScope'; +import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; + +export const RecordBoardHotkeyEffect = () => { + const { recordBoardId } = useContext(RecordBoardContext); + + const { move } = useRecordBoardCardNavigation(recordBoardId); + + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRecordIdsComponentSelector, + ); + + const { setRecordAsSelected } = useRecordBoardSelection(recordBoardId); + + const selectAll = useRecoilCallback( + ({ snapshot }) => + () => { + const allRecordIds = getSnapshotValue( + snapshot, + recordIndexAllRecordIdsState, + ); + + for (const recordId of allRecordIds) { + setRecordAsSelected(recordId, true); + } + }, + [recordIndexAllRecordIdsState, setRecordAsSelected], + ); + + useScopedHotkeys( + 'ctrl+a,meta+a', + selectAll, + RecordIndexHotkeyScope.RecordIndex, + ); + + useScopedHotkeys('ctrl+a,meta+a', selectAll, BoardHotkeyScope.BoardFocus); + + useScopedHotkeys( + Key.ArrowLeft, + () => { + setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + move('left'); + }, + RecordIndexHotkeyScope.RecordIndex, + ); + + useScopedHotkeys( + Key.ArrowRight, + () => { + setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + move('right'); + }, + RecordIndexHotkeyScope.RecordIndex, + ); + + useScopedHotkeys( + Key.ArrowUp, + () => { + setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + move('up'); + }, + RecordIndexHotkeyScope.RecordIndex, + ); + + useScopedHotkeys( + Key.ArrowDown, + () => { + setHotkeyScopeAndMemorizePreviousScope(BoardHotkeyScope.BoardFocus); + move('down'); + }, + RecordIndexHotkeyScope.RecordIndex, + ); + + useScopedHotkeys( + Key.ArrowLeft, + () => { + move('left'); + }, + BoardHotkeyScope.BoardFocus, + ); + + useScopedHotkeys( + Key.ArrowRight, + () => { + move('right'); + }, + BoardHotkeyScope.BoardFocus, + ); + + useScopedHotkeys( + Key.ArrowUp, + () => { + move('up'); + }, + BoardHotkeyScope.BoardFocus, + ); + + useScopedHotkeys( + Key.ArrowDown, + () => { + move('down'); + }, + BoardHotkeyScope.BoardFocus, + ); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect.tsx new file mode 100644 index 00000000000..1d6cdbf1dc7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; + +import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; +import { focusedRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState'; +import { isRecordBoardCardFocusActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState'; +import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordBoardScrollToFocusedCardEffect = () => { + const recordBoardId = useAvailableScopeIdOrThrow( + RecordBoardScopeInternalContext, + ); + + const focusedCardIndexes = useRecoilComponentValueV2( + focusedRecordBoardCardIndexesComponentState, + recordBoardId, + ); + + const isFocusActive = useRecoilComponentValueV2( + isRecordBoardCardFocusActiveComponentState, + recordBoardId, + ); + + useEffect(() => { + if (!isFocusActive || !focusedCardIndexes) { + return; + } + + const { rowIndex, columnIndex } = focusedCardIndexes; + + const focusElement = document.getElementById( + `record-board-card-${columnIndex}-${rowIndex}`, + ); + + if (!focusElement) { + return; + } + + if (focusElement instanceof HTMLElement) { + focusElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }, [focusedCardIndexes, isFocusActive]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useActiveRecordBoardCard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useActiveRecordBoardCard.ts new file mode 100644 index 00000000000..03ce8f62355 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useActiveRecordBoardCard.ts @@ -0,0 +1,64 @@ +import { activeRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/activeRecordBoardCardIndexesComponentState'; +import { isRecordBoardCardActiveComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useActiveRecordBoardCard = (recordBoardId?: string) => { + const isCardActiveState = useRecoilComponentCallbackStateV2( + isRecordBoardCardActiveComponentFamilyState, + recordBoardId, + ); + + const activeBoardCardIndexesState = useRecoilComponentCallbackStateV2( + activeRecordBoardCardIndexesComponentState, + recordBoardId, + ); + + const deactivateBoardCard = useRecoilCallback( + ({ set, snapshot }) => + () => { + const activeBoardCardIndexes = snapshot + .getLoadable(activeBoardCardIndexesState) + .getValue(); + + if (!isDefined(activeBoardCardIndexes)) { + return; + } + + set(activeBoardCardIndexesState, null); + set(isCardActiveState(activeBoardCardIndexes), false); + }, + [activeBoardCardIndexesState, isCardActiveState], + ); + + const activateBoardCard = useRecoilCallback( + ({ set, snapshot }) => + (boardCardIndexes: BoardCardIndexes) => { + const activeBoardCardIndexes = snapshot + .getLoadable(activeBoardCardIndexesState) + .getValue(); + + if ( + activeBoardCardIndexes?.rowIndex === boardCardIndexes.rowIndex && + activeBoardCardIndexes?.columnIndex === boardCardIndexes.columnIndex + ) { + return; + } + + if (isDefined(activeBoardCardIndexes)) { + set(isCardActiveState(activeBoardCardIndexes), false); + } + + set(activeBoardCardIndexesState, boardCardIndexes); + set(isCardActiveState(boardCardIndexes), true); + }, + [activeBoardCardIndexesState, isCardActiveState], + ); + + return { + activateBoardCard, + deactivateBoardCard, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts new file mode 100644 index 00000000000..059d335379a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts @@ -0,0 +1,70 @@ +import { focusedRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState'; +import { isRecordBoardCardFocusActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState'; +import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useFocusedRecordBoardCard = (recordBoardId?: string) => { + const isCardFocusedState = useRecoilComponentCallbackStateV2( + isRecordBoardCardFocusedComponentFamilyState, + recordBoardId, + ); + + const focusedBoardCardIndexesState = useRecoilComponentCallbackStateV2( + focusedRecordBoardCardIndexesComponentState, + recordBoardId, + ); + + const isCardFocusActiveState = useRecoilComponentCallbackStateV2( + isRecordBoardCardFocusActiveComponentState, + recordBoardId, + ); + + const unfocusBoardCard = useRecoilCallback( + ({ set, snapshot }) => + () => { + const focusedBoardCardIndexes = snapshot + .getLoadable(focusedBoardCardIndexesState) + .getValue(); + + if (!isDefined(focusedBoardCardIndexes)) { + return; + } + + set(focusedBoardCardIndexesState, null); + set(isCardFocusedState(focusedBoardCardIndexes), false); + set(isCardFocusActiveState, false); + }, + [focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState], + ); + + const focusBoardCard = useRecoilCallback( + ({ set, snapshot }) => + (boardCardIndexes: BoardCardIndexes) => { + const focusedBoardCardIndexes = snapshot + .getLoadable(focusedBoardCardIndexesState) + .getValue(); + + if ( + isDefined(focusedBoardCardIndexes) && + (focusedBoardCardIndexes.rowIndex !== boardCardIndexes.rowIndex || + focusedBoardCardIndexes.columnIndex !== + boardCardIndexes.columnIndex) + ) { + set(isCardFocusedState(focusedBoardCardIndexes), false); + } + + set(focusedBoardCardIndexesState, boardCardIndexes); + set(isCardFocusedState(boardCardIndexes), true); + set(isCardFocusActiveState, true); + }, + [focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState], + ); + + return { + focusBoardCard, + unfocusBoardCard, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardNavigation.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardNavigation.ts new file mode 100644 index 00000000000..5dc486d1cf5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardNavigation.ts @@ -0,0 +1,216 @@ +import { useRecoilCallback } from 'recoil'; + +import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; +import { focusedRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { ViewType } from '@/views/types/ViewType'; +import { isDefined } from 'twenty-shared/utils'; + +type NavigationDirection = 'up' | 'down' | 'left' | 'right'; + +export const useRecordBoardCardNavigation = (recordBoardId?: string) => { + const { focusBoardCard } = useFocusedRecordBoardCard(recordBoardId); + + const focusedBoardCardIndexesState = useRecoilComponentCallbackStateV2( + focusedRecordBoardCardIndexesComponentState, + recordBoardId, + ); + + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, + ); + + const recordIdsByGroupState = useRecoilComponentCallbackStateV2( + recordIndexRecordIdsByGroupComponentFamilyState, + ); + + const moveHorizontally = useRecoilCallback( + ({ snapshot }) => + (direction: 'left' | 'right') => { + const focusedBoardCardIndexes = snapshot + .getLoadable(focusedBoardCardIndexesState) + .getValue(); + + if (!isDefined(focusedBoardCardIndexes)) { + if (visibleRecordGroupIds.length === 0) { + return; + } + + const firstGroupId = visibleRecordGroupIds[0]; + const recordIdsInFirstGroup = snapshot + .getLoadable(recordIdsByGroupState(firstGroupId)) + .getValue(); + + if ( + !Array.isArray(recordIdsInFirstGroup) || + recordIdsInFirstGroup.length === 0 + ) { + return; + } + + focusBoardCard({ + columnIndex: 0, + rowIndex: 0, + }); + return; + } + + if (visibleRecordGroupIds.length === 0) { + return; + } + + let newColumnIndex = + direction === 'right' + ? focusedBoardCardIndexes.columnIndex + 1 + : focusedBoardCardIndexes.columnIndex - 1; + + if (newColumnIndex < 0) { + newColumnIndex = 0; + } else if (newColumnIndex >= visibleRecordGroupIds.length) { + newColumnIndex = visibleRecordGroupIds.length - 1; + } + + if (newColumnIndex === focusedBoardCardIndexes.columnIndex) { + return; + } + + let foundColumnWithRecords = false; + const initialColumnIndex = newColumnIndex; + + while (!foundColumnWithRecords) { + const currentGroupId = visibleRecordGroupIds[newColumnIndex]; + const recordIdsInGroup = snapshot + .getLoadable(recordIdsByGroupState(currentGroupId)) + .getValue(); + + if (Array.isArray(recordIdsInGroup) && recordIdsInGroup.length > 0) { + foundColumnWithRecords = true; + } else { + newColumnIndex = + direction === 'right' ? newColumnIndex + 1 : newColumnIndex - 1; + + if ( + newColumnIndex < 0 || + newColumnIndex >= visibleRecordGroupIds.length + ) { + return; + } + + if ( + (direction === 'right' && newColumnIndex <= initialColumnIndex) || + (direction === 'left' && newColumnIndex >= initialColumnIndex) + ) { + return; + } + } + } + + const currentGroupId = visibleRecordGroupIds[newColumnIndex]; + const recordIdsInGroup = snapshot + .getLoadable(recordIdsByGroupState(currentGroupId)) + .getValue(); + + let newRowIndex = focusedBoardCardIndexes.rowIndex; + if (newRowIndex >= recordIdsInGroup.length) { + newRowIndex = recordIdsInGroup.length - 1; + } + + focusBoardCard({ + columnIndex: newColumnIndex, + rowIndex: newRowIndex, + }); + }, + [ + focusedBoardCardIndexesState, + visibleRecordGroupIds, + recordIdsByGroupState, + focusBoardCard, + ], + ); + + const moveVertically = useRecoilCallback( + ({ snapshot }) => + (direction: 'up' | 'down') => { + const focusedBoardCardIndexes = snapshot + .getLoadable(focusedBoardCardIndexesState) + .getValue(); + + if (!isDefined(focusedBoardCardIndexes)) { + if (visibleRecordGroupIds.length === 0) return; + + const firstGroupId = visibleRecordGroupIds[0]; + const recordIdsInFirstGroup = snapshot + .getLoadable(recordIdsByGroupState(firstGroupId)) + .getValue(); + + if ( + !Array.isArray(recordIdsInFirstGroup) || + recordIdsInFirstGroup.length === 0 + ) { + return; + } + + focusBoardCard({ + columnIndex: 0, + rowIndex: 0, + }); + + return; + } + + if (visibleRecordGroupIds.length === 0) return; + + const currentGroupId = + visibleRecordGroupIds[focusedBoardCardIndexes.columnIndex]; + const recordIdsInGroup = snapshot + .getLoadable(recordIdsByGroupState(currentGroupId)) + .getValue(); + + if (!Array.isArray(recordIdsInGroup) || recordIdsInGroup.length === 0) { + return; + } + + let newRowIndex = + direction === 'down' + ? focusedBoardCardIndexes.rowIndex + 1 + : focusedBoardCardIndexes.rowIndex - 1; + + if (newRowIndex < 0) { + newRowIndex = 0; + } else if (newRowIndex >= recordIdsInGroup.length) { + newRowIndex = recordIdsInGroup.length - 1; + } + + if (newRowIndex === focusedBoardCardIndexes.rowIndex) { + return; + } + + focusBoardCard({ + columnIndex: focusedBoardCardIndexes.columnIndex, + rowIndex: newRowIndex, + }); + }, + [ + focusedBoardCardIndexesState, + visibleRecordGroupIds, + recordIdsByGroupState, + focusBoardCard, + ], + ); + + const move = (direction: NavigationDirection) => { + if (direction === 'left' || direction === 'right') { + moveHorizontally(direction); + } else if (direction === 'up' || direction === 'down') { + moveVertically(direction); + } + }; + + return { + move, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index fc7a42347f4..f3dea2e1753 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -2,13 +2,20 @@ import { useRecoilCallback } from 'recoil'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -export const useRecordBoardSelection = (recordBoardId: string) => { +export const useRecordBoardSelection = (recordBoardId?: string) => { + const instanceIdFromProps = useAvailableComponentInstanceIdOrThrow( + RecordBoardComponentInstanceContext, + recordBoardId, + ); + const isRecordBoardCardSelectedFamilyState = useRecoilComponentCallbackStateV2( isRecordBoardCardSelectedComponentFamilyState, @@ -24,7 +31,7 @@ export const useRecordBoardSelection = (recordBoardId: string) => { const { closeDropdown } = useDropdownV2(); const dropdownId = getActionMenuDropdownIdFromActionMenuId( - getActionMenuIdFromRecordIndexId(recordBoardId), + getActionMenuIdFromRecordIndexId(instanceIdFromProps), ); const resetRecordSelection = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index d066372b343..b77f0a16680 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -3,6 +3,8 @@ import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/get import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; +import { isRecordBoardCardActiveComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState'; +import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; @@ -19,6 +21,7 @@ import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; @@ -30,27 +33,44 @@ import { AnimatedEaseInOut } from 'twenty-ui/utilities'; import { useDebouncedCallback } from 'use-debounce'; import { useNavigateApp } from '~/hooks/useNavigateApp'; -const StyledBoardCard = styled.div<{ selected: boolean }>` - background-color: ${({ theme, selected }) => - selected ? theme.accent.quaternary : theme.background.secondary}; - border: 1px solid - ${({ theme, selected }) => - selected ? theme.adaptiveColors.blue3 : theme.border.color.medium}; +const StyledBoardCard = styled.div<{ + isDragging?: boolean; +}>` + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; box-shadow: ${({ theme }) => theme.boxShadow.light}; color: ${({ theme }) => theme.font.color.primary}; - &:hover { - background-color: ${({ theme, selected }) => - selected && theme.accent.tertiary}; - border: 1px solid - ${({ theme, selected }) => - selected ? theme.adaptiveColors.blue3 : theme.border.color.strong}; - } cursor: pointer; + &[data-selected='true'] { + background-color: ${({ theme }) => theme.accent.quaternary}; + } + + &[data-focused='true'] { + background-color: ${({ theme }) => theme.background.tertiary}; + } + + &[data-active='true'] { + background-color: ${({ theme }) => theme.accent.quaternary}; + border: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + } + + &:hover { + border: 1px solid ${({ theme }) => theme.border.color.strong}; + + &[data-active='true'] { + border: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; + } + } + .checkbox-container { transition: all ease-in-out 160ms; - opacity: ${({ selected }) => (selected ? 1 : 0)}; + opacity: 0; + } + + &[data-selected='true'] .checkbox-container { + opacity: 1; } &:hover .checkbox-container { @@ -75,7 +95,9 @@ export const RecordBoardCard = () => { const navigate = useNavigateApp(); const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); - const { recordId } = useContext(RecordBoardCardContext); + const { recordId, rowIndex, columnIndex } = useContext( + RecordBoardCardContext, + ); const visibleFieldDefinitions = useRecoilComponentValueV2( recordBoardVisibleFieldDefinitionsComponentSelector, @@ -93,6 +115,22 @@ export const RecordBoardCard = () => { recordId, ); + const isCurrentCardFocused = useRecoilComponentFamilyValueV2( + isRecordBoardCardFocusedComponentFamilyState, + { + rowIndex, + columnIndex, + }, + ); + + const isCurrentCardActive = useRecoilComponentFamilyValueV2( + isRecordBoardCardActiveComponentFamilyState, + { + rowIndex, + columnIndex, + }, + ); + const { objectNameSingular } = useRecordIndexContextOrThrow(); const recordBoardId = useAvailableScopeIdOrThrow( @@ -167,7 +205,9 @@ export const RecordBoardCard = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx index ccced3426be..7219f60b169 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx @@ -1,25 +1,50 @@ +import styled from '@emotion/styled'; import { Draggable } from '@hello-pangea/dnd'; +import { useContext } from 'react'; import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard'; +import { RecordBoardCardFocusHotkeyEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; +import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; + +const StyledDraggableContainer = styled.div` + scroll-margin-left: 8px; + scroll-margin-right: 8px; + scroll-margin-top: 40px; +`; export const RecordBoardCardDraggableContainer = ({ recordId, - index, + rowIndex, }: { recordId: string; - index: number; + rowIndex: number; }) => { const isRecordReadOnly = useIsRecordReadOnly({ recordId, }); + const { columnIndex } = useContext(RecordBoardColumnContext); + + const isRecordBoardCardFocusActive = useRecoilComponentFamilyValueV2( + isRecordBoardCardFocusedComponentFamilyState, + { + rowIndex, + columnIndex, + }, + ); + return ( - - + + {(draggableProvided) => ( -
+ {isRecordBoardCardFocusActive && ( + + )} -
+ )}
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx new file mode 100644 index 00000000000..fffa7eb1178 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx @@ -0,0 +1,57 @@ +import { useContext } from 'react'; +import { Key } from 'ts-key-enum'; + +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; +import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { BoardHotkeyScope } from '@/object-record/record-board/types/BoardHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +export const RecordBoardCardFocusHotkeyEffect = () => { + const { objectMetadataItem } = useContext(RecordBoardContext); + + const { recordId, rowIndex, columnIndex } = useContext( + RecordBoardCardContext, + ); + + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); + + const { activateBoardCard } = useActiveRecordBoardCard(); + + const { setRecordAsSelected } = useRecordBoardSelection(); + + const isRecordBoardCardSelected = useRecoilComponentFamilyValueV2( + isRecordBoardCardSelectedComponentFamilyState, + recordId, + ); + + useScopedHotkeys( + 'x', + () => { + setRecordAsSelected(recordId, !isRecordBoardCardSelected); + }, + BoardHotkeyScope.BoardFocus, + ); + + useScopedHotkeys( + [Key.Enter, `${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`], + () => { + openRecordInCommandMenu({ + recordId: recordId, + objectNameSingular: objectMetadataItem.nameSingular, + isNewRecord: false, + }); + + activateBoardCard({ + rowIndex, + columnIndex, + }); + }, + BoardHotkeyScope.BoardFocus, + ); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect.tsx new file mode 100644 index 00000000000..52fb109877f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect.tsx @@ -0,0 +1,15 @@ +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; +import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose'; +import { useContext } from 'react'; + +export const RecordBoardDeactivateBoardCardEffect = () => { + const { recordBoardId } = useContext(RecordBoardContext); + const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId); + + useListenRightDrawerClose(() => { + deactivateBoardCard(); + }); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts index db5c0ce779d..ac038962fe6 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts @@ -3,6 +3,8 @@ import { createContext } from 'react'; type RecordBoardCardContextProps = { recordId: string; isRecordReadOnly: boolean; + rowIndex: number; + columnIndex: number; }; export const RecordBoardCardContext = diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 2bde694252b..313a5cc4b13 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -22,10 +22,12 @@ const StyledColumn = styled.div` type RecordBoardColumnProps = { recordBoardColumnId: string; + recordBoardColumnIndex: number; }; export const RecordBoardColumn = ({ recordBoardColumnId, + recordBoardColumnIndex, }: RecordBoardColumnProps) => { const recordGroupDefinition = useRecoilValue( recordGroupDefinitionFamilyState(recordBoardColumnId), @@ -46,6 +48,7 @@ export const RecordBoardColumn = ({ columnDefinition: recordGroupDefinition, columnId: recordBoardColumnId, recordIds: recordIdsByGroup, + columnIndex: recordBoardColumnIndex, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx index 815b74c6413..d0967551df1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx @@ -12,7 +12,7 @@ export const RecordBoardColumnCardsMemo = React.memo( )); }, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx index 80b26329747..e9952130db3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx @@ -8,10 +8,12 @@ import { isDefined } from 'twenty-shared/utils'; type RecordBoardColumnHeaderWrapperProps = { columnId: string; + columnIndex: number; }; export const RecordBoardColumnHeaderWrapper = ({ columnId, + columnIndex, }: RecordBoardColumnHeaderWrapperProps) => { const recordGroupDefinition = useRecoilValue( recordGroupDefinitionFamilyState(columnId), @@ -32,6 +34,7 @@ export const RecordBoardColumnHeaderWrapper = ({ columnId, columnDefinition: recordGroupDefinition, recordIds: recordIdsByGroup, + columnIndex, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts index 4140e0cf18e..568170bea3b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts @@ -6,6 +6,7 @@ type RecordBoardColumnContextProps = { columnDefinition: RecordGroupDefinition; columnId: string; recordIds: string[]; + columnIndex: number; }; export const RecordBoardColumnContext = diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/activeRecordBoardCardIndexesComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/activeRecordBoardCardIndexesComponentState.ts new file mode 100644 index 00000000000..be299045fe9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/activeRecordBoardCardIndexesComponentState.ts @@ -0,0 +1,10 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const activeRecordBoardCardIndexesComponentState = + createComponentStateV2({ + key: 'activeRecordBoardCardIndexesComponentState', + defaultValue: null, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState.ts new file mode 100644 index 00000000000..3809d3aae37 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState.ts @@ -0,0 +1,10 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const focusedRecordBoardCardIndexesComponentState = + createComponentStateV2({ + key: 'focusedRecordBoardCardIndexesComponentState', + defaultValue: null, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState.ts new file mode 100644 index 00000000000..0cdc0103621 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardActiveComponentFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const isRecordBoardCardActiveComponentFamilyState = + createComponentFamilyStateV2({ + key: 'isRecordBoardCardActiveComponentFamilyState', + defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState.ts new file mode 100644 index 00000000000..9a943c6e9fb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState.ts @@ -0,0 +1,9 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const isRecordBoardCardFocusActiveComponentState = + createComponentStateV2({ + key: 'isRecordBoardCardFocusActiveComponentState', + defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState.ts new file mode 100644 index 00000000000..ab697d923d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const isRecordBoardCardFocusedComponentFamilyState = + createComponentFamilyStateV2({ + key: 'isRecordBoardCardFocusedComponentFamilyState', + defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/BoardCardIndexes.ts b/packages/twenty-front/src/modules/object-record/record-board/types/BoardCardIndexes.ts new file mode 100644 index 00000000000..b7aee0104c0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/types/BoardCardIndexes.ts @@ -0,0 +1,4 @@ +export type BoardCardIndexes = { + rowIndex: number; + columnIndex: number; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/BoardHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-board/types/BoardHotkeyScope.ts new file mode 100644 index 00000000000..b58840d74fa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/types/BoardHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum BoardHotkeyScope { + BoardFocus = 'board-focus', +} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index edd219383f0..3a9426497b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -5,10 +5,11 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordBoard } from '@/object-record/record-board/components/RecordBoard'; +import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect'; +import { RecordBoardHotkeyEffect } from '@/object-record/record-board/components/RecordBoardHotkeyEffect'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; - type RecordIndexBoardContainerProps = { recordBoardId: string; viewBarId: string; @@ -55,6 +56,8 @@ export const RecordIndexBoardContainer = ({ > + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/types/RecordIndexHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-index/types/RecordIndexHotkeyScope.ts new file mode 100644 index 00000000000..fcdd9d87f35 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/types/RecordIndexHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum RecordIndexHotkeyScope { + RecordIndex = 'record-index', +} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index fd07bc2910b..16e71e5c774 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -10,6 +10,7 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; @@ -50,7 +51,17 @@ export const RecordTableWithWrappers = ({ useScopedHotkeys( 'ctrl+a,meta+a', handleSelectAllRows, - TableHotkeyScope.Table, + RecordIndexHotkeyScope.RecordIndex, + [], + { + enableOnFormTags: false, + }, + ); + + useScopedHotkeys( + 'ctrl+a,meta+a', + handleSelectAllRows, + TableHotkeyScope.TableFocus, [], { enableOnFormTags: false, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 20283baf3c0..f59ee32a1a9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -1,10 +1,10 @@ +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; -import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -49,6 +49,6 @@ export const useLeaveTableFocus = (recordTableId?: string) => { setRecordTableHoverPosition(null); - setHotkeyScope(TableHotkeyScope.Table); + setHotkeyScope(RecordIndexHotkeyScope.RecordIndex); }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 61970bafd5c..93a12a9b508 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -7,6 +7,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState'; import { ColumnDefinition } from '../types/ColumnDefinition'; import { TableHotkeyScope } from '../types/TableHotkeyScope'; @@ -168,7 +169,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); move('up'); }, - TableHotkeyScope.Table, + RecordIndexHotkeyScope.RecordIndex, [move], ); @@ -178,7 +179,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus); move('down'); }, - TableHotkeyScope.Table, + RecordIndexHotkeyScope.RecordIndex, [move], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx index 0fa9d56e311..78c3edf07d8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect.tsx @@ -1,12 +1,13 @@ import { Key } from 'ts-key-enum'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector'; -import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + export const RecordTableBodyEscapeHotkeyEffect = () => { const { recordTableId } = useRecordTableContextOrThrow(); @@ -28,9 +29,9 @@ export const RecordTableBodyEscapeHotkeyEffect = () => { resetTableRowSelection(); } }, - TableHotkeyScope.Table, + RecordIndexHotkeyScope.RecordIndex, [isAtLeastOneRecordSelected, resetTableRowSelection, unfocusRecordTableRow], ); - return <>; + return null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx index 08424ac9cf7..98ae39819ff 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusKeyboardEffect.tsx @@ -1,3 +1,4 @@ +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -37,7 +38,7 @@ export const RecordTableBodyFocusKeyboardEffect = () => { restoreRecordTableRowFocusFromCellPosition(); setIsFocusActiveForCurrentPosition(false); } else { - setHotkeyScope(TableHotkeyScope.Table, { + setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, { goto: true, keyboardShortcutMenu: true, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell.ts index efb848e8d62..a8e4f1e2e7d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveHoverToCurrentCell.ts @@ -4,6 +4,7 @@ import { TableCellPosition } from '@/object-record/record-table/types/TableCellP import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; @@ -38,8 +39,8 @@ export const useMoveHoverToCurrentCell = (recordTableId: string) => { if ( currentHotkeyScope.scope !== TableHotkeyScope.TableFocus && currentHotkeyScope.scope !== TableHotkeyScope.CellEditMode && - currentHotkeyScope.scope !== TableHotkeyScope.Table && - currentHotkeyScope.scope !== AppHotkeyScope.CommandMenuOpen + currentHotkeyScope.scope !== AppHotkeyScope.CommandMenuOpen && + currentHotkeyScope.scope !== RecordIndexHotkeyScope.RecordIndex ) { return; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx index 30158014367..2e557199177 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -139,13 +139,9 @@ export const RecordTableTr = forwardRef< data-virtualized-id={recordId} isDragging={isDragging} ref={ref} - data-active={isActive ? 'true' : 'false'} - data-focused={ - isRowFocusActive && isFocused && !isActive ? 'true' : 'false' - } - data-next-row-active-or-focused={ - isNextRowActiveOrFocused ? 'true' : 'false' - } + data-active={isActive} + data-focused={isRowFocusActive && isFocused && !isActive} + data-next-row-active-or-focused={isNextRowActiveOrFocused} // eslint-disable-next-line react/jsx-props-no-spreading {...props} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/types/TableHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-table/types/TableHotkeyScope.ts index b1bae4957ee..ad2fe85eb91 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/types/TableHotkeyScope.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/types/TableHotkeyScope.ts @@ -3,5 +3,4 @@ export enum TableHotkeyScope { CellEditMode = 'cell-edit-mode', CellDateEditMode = 'cell-date-edit-mode', TableFocus = 'table-focus', - Table = 'table', }