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 14787efc4e..c21113deac 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 d997125906..0749116df2 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 36bb69231c..c92fb987db 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 49d98de154..d38b3b5774 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 347e40db0b..0d568a4735 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 c87033b9b6..04b57f57fa 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 0000000000..717aaf11f3 --- /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 3ca0b6e680..187606a64f 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 0000000000..6ceebcdc10 --- /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 0000000000..1d6cdbf1dc --- /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 0000000000..03ce8f6235 --- /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 0000000000..059d335379 --- /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 0000000000..5dc486d1cf --- /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 fc7a42347f..f3dea2e175 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 d066372b34..b77f0a1668 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 ccced3426b..7219f60b16 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 0000000000..fffa7eb117 --- /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 0000000000..52fb109877 --- /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 db5c0ce779..ac038962fe 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 2bde694252..313a5cc4b1 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 815b74c641..d0967551df 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 80b2632974..e9952130db 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 4140e0cf18..568170bea3 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 0000000000..be299045fe --- /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 0000000000..3809d3aae3 --- /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 0000000000..0cdc010362 --- /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 0000000000..9a943c6e9f --- /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 0000000000..ab697d923d --- /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 0000000000..b7aee0104c --- /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 0000000000..b58840d74f --- /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 edd219383f..3a9426497b 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 0000000000..fcdd9d87f3 --- /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 fd07bc2910..16e71e5c77 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 20283baf3c..f59ee32a1a 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 61970bafd5..93a12a9b50 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 0fa9d56e31..78c3edf07d 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 08424ac9cf..98ae39819f 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 efb848e8d6..a8e4f1e2e7 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 3015801436..2e55719917 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 b1bae4957e..ad2fe85eb9 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', }