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
This commit is contained in:
@ -33,9 +33,8 @@ export const ActionModal = ({
|
||||
const { closeActionMenu } = useCloseActionMenu();
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
closeActionMenu();
|
||||
onConfirmClick();
|
||||
setIsOpen(false);
|
||||
closeActionMenu();
|
||||
};
|
||||
|
||||
const actionConfig = useContext(ActionConfigContext);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`}
|
||||
>
|
||||
<RecordBoardStickyHeaderEffect />
|
||||
<RecordBoardScrollToFocusedCardEffect />
|
||||
<RecordBoardDeactivateBoardCardEffect />
|
||||
<StyledContainerContainer>
|
||||
<RecordBoardHeader />
|
||||
<StyledBoardContentContainer>
|
||||
<StyledContainer ref={boardRef}>
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<StyledColumnContainer>
|
||||
{visibleRecordGroupIds.map((recordGroupId) => (
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordBoardColumn
|
||||
key={recordGroupId}
|
||||
recordBoardColumnId={recordGroupId}
|
||||
recordBoardColumnIndex={index}
|
||||
/>
|
||||
))}
|
||||
</StyledColumnContainer>
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -31,9 +31,10 @@ export const RecordBoardHeader = () => {
|
||||
|
||||
return (
|
||||
<StyledHeaderContainer id="record-board-header">
|
||||
{visibleRecordGroupIds.map((recordGroupId) => (
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordBoardColumnHeaderWrapper
|
||||
columnId={recordGroupId}
|
||||
columnIndex={index}
|
||||
key={recordGroupId}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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(
|
||||
|
||||
@ -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 = () => {
|
||||
<InView>
|
||||
<StyledBoardCard
|
||||
ref={cardRef}
|
||||
selected={isCurrentCardSelected}
|
||||
data-selected={isCurrentCardSelected}
|
||||
data-focused={isCurrentCardFocused}
|
||||
data-active={isCurrentCardActive}
|
||||
onMouseLeave={onMouseLeaveBoard}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
|
||||
@ -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 (
|
||||
<RecordBoardCardContext.Provider value={{ recordId, isRecordReadOnly }}>
|
||||
<Draggable key={recordId} draggableId={recordId} index={index}>
|
||||
<RecordBoardCardContext.Provider
|
||||
value={{ recordId, isRecordReadOnly, rowIndex, columnIndex }}
|
||||
>
|
||||
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
<StyledDraggableContainer
|
||||
id={`record-board-card-${columnIndex}-${rowIndex}`}
|
||||
ref={draggableProvided?.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.dragHandleProps}
|
||||
@ -29,8 +54,11 @@ export const RecordBoardCardDraggableContainer = ({
|
||||
data-selectable-id={recordId}
|
||||
data-select-disable
|
||||
>
|
||||
{isRecordBoardCardFocusActive && (
|
||||
<RecordBoardCardFocusHotkeyEffect />
|
||||
)}
|
||||
<RecordBoardCard />
|
||||
</div>
|
||||
</StyledDraggableContainer>
|
||||
)}
|
||||
</Draggable>
|
||||
</RecordBoardCardContext.Provider>
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -3,6 +3,8 @@ import { createContext } from 'react';
|
||||
type RecordBoardCardContextProps = {
|
||||
recordId: string;
|
||||
isRecordReadOnly: boolean;
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
};
|
||||
|
||||
export const RecordBoardCardContext =
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
<Droppable droppableId={recordBoardColumnId}>
|
||||
|
||||
@ -12,7 +12,7 @@ export const RecordBoardColumnCardsMemo = React.memo(
|
||||
<RecordBoardCardDraggableContainer
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
index={index}
|
||||
rowIndex={index}
|
||||
/>
|
||||
));
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
<RecordBoardColumnHeader />
|
||||
|
||||
@ -6,6 +6,7 @@ type RecordBoardColumnContextProps = {
|
||||
columnDefinition: RecordGroupDefinition;
|
||||
columnId: string;
|
||||
recordIds: string[];
|
||||
columnIndex: number;
|
||||
};
|
||||
|
||||
export const RecordBoardColumnContext =
|
||||
|
||||
@ -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<BoardCardIndexes | null>({
|
||||
key: 'activeRecordBoardCardIndexesComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
@ -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<BoardCardIndexes | null>({
|
||||
key: 'focusedRecordBoardCardIndexesComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean, BoardCardIndexes>({
|
||||
key: 'isRecordBoardCardActiveComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean>({
|
||||
key: 'isRecordBoardCardFocusActiveComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
@ -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<boolean, BoardCardIndexes>({
|
||||
key: 'isRecordBoardCardFocusedComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: RecordBoardComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
export type BoardCardIndexes = {
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum BoardHotkeyScope {
|
||||
BoardFocus = 'board-focus',
|
||||
}
|
||||
@ -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 = ({
|
||||
>
|
||||
<RecordBoard />
|
||||
<RecordIndexRemoveSortingModal />
|
||||
<RecordBoardHotkeyEffect />
|
||||
<RecordBoardBodyEscapeHotkeyEffect />
|
||||
</RecordBoardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export enum RecordIndexHotkeyScope {
|
||||
RecordIndex = 'record-index',
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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],
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -3,5 +3,4 @@ export enum TableHotkeyScope {
|
||||
CellEditMode = 'cell-edit-mode',
|
||||
CellDateEditMode = 'cell-date-edit-mode',
|
||||
TableFocus = 'table-focus',
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user