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 { closeActionMenu } = useCloseActionMenu();
|
||||||
|
|
||||||
const handleConfirmClick = () => {
|
const handleConfirmClick = () => {
|
||||||
closeActionMenu();
|
|
||||||
onConfirmClick();
|
onConfirmClick();
|
||||||
setIsOpen(false);
|
closeActionMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionConfig = useContext(ActionConfigContext);
|
const actionConfig = useContext(ActionConfigContext);
|
||||||
|
|||||||
@ -17,11 +17,16 @@ import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoad
|
|||||||
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
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 { 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 { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
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 { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||||
import { AppBasePath } from '@/types/AppBasePath';
|
import { AppBasePath } from '@/types/AppBasePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
@ -64,6 +69,11 @@ export const PageChangeEffect = () => {
|
|||||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const contextStoreCurrentViewType = useRecoilComponentValueV2(
|
||||||
|
contextStoreCurrentViewTypeComponentState,
|
||||||
|
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||||
|
);
|
||||||
|
|
||||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
contextStoreCurrentViewId || '',
|
contextStoreCurrentViewId || '',
|
||||||
@ -73,6 +83,10 @@ export const PageChangeEffect = () => {
|
|||||||
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
|
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
|
||||||
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
|
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
|
||||||
|
|
||||||
|
const { resetRecordSelection } = useRecordBoardSelection(recordIndexId);
|
||||||
|
const { deactivateBoardCard } = useActiveRecordBoardCard(recordIndexId);
|
||||||
|
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordIndexId);
|
||||||
|
|
||||||
const { executeTasksOnAnyLocationChange } =
|
const { executeTasksOnAnyLocationChange } =
|
||||||
useExecuteTasksOnAnyLocationChange();
|
useExecuteTasksOnAnyLocationChange();
|
||||||
|
|
||||||
@ -100,9 +114,16 @@ export const PageChangeEffect = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isLeavingRecordIndexPage) {
|
if (isLeavingRecordIndexPage) {
|
||||||
resetTableSelections();
|
if (contextStoreCurrentViewType === ContextStoreViewType.Table) {
|
||||||
unfocusRecordTableRow();
|
resetTableSelections();
|
||||||
deactivateRecordTableRow();
|
unfocusRecordTableRow();
|
||||||
|
deactivateRecordTableRow();
|
||||||
|
}
|
||||||
|
if (contextStoreCurrentViewType === ContextStoreViewType.Kanban) {
|
||||||
|
resetRecordSelection();
|
||||||
|
deactivateBoardCard();
|
||||||
|
unfocusBoardCard();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isMatchingLocation,
|
isMatchingLocation,
|
||||||
@ -110,12 +131,16 @@ export const PageChangeEffect = () => {
|
|||||||
resetTableSelections,
|
resetTableSelections,
|
||||||
unfocusRecordTableRow,
|
unfocusRecordTableRow,
|
||||||
deactivateRecordTableRow,
|
deactivateRecordTableRow,
|
||||||
|
contextStoreCurrentViewType,
|
||||||
|
resetRecordSelection,
|
||||||
|
deactivateBoardCard,
|
||||||
|
unfocusBoardCard,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isMatchingLocation(AppPath.RecordIndexPage): {
|
case isMatchingLocation(AppPath.RecordIndexPage): {
|
||||||
setHotkeyScope(TableHotkeyScope.Table, {
|
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -51,6 +51,10 @@ export const useNavigateCommandMenu = () => {
|
|||||||
commandMenuCloseAnimationCompleteCleanup();
|
commandMenuCloseAnimationCompleteCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCommandMenuOpened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
CommandMenuHotkeyScope.CommandMenuFocused,
|
CommandMenuHotkeyScope.CommandMenuFocused,
|
||||||
{
|
{
|
||||||
@ -58,10 +62,6 @@ export const useNavigateCommandMenu = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isCommandMenuOpened) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyContextStoreStates({
|
copyContextStoreStates({
|
||||||
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||||
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { expect } from '@storybook/test';
|
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 { RecoilRoot, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { isKeyboardShortcutMenuOpenedState } from '@/keyboard-shortcut-menu/states/isKeyboardShortcutMenuOpenedState';
|
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
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';
|
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
|
||||||
|
|
||||||
export const useKeyboardShortcutMenu = () => {
|
export const useKeyboardShortcutMenu = () => {
|
||||||
const [, setIsKeyboardShortcutMenuOpened] = useRecoilState(
|
|
||||||
isKeyboardShortcutMenuOpenedState,
|
|
||||||
);
|
|
||||||
const isKeyboardShortcutMenuOpened = useRecoilValue(
|
|
||||||
isKeyboardShortcutMenuOpenedState,
|
|
||||||
);
|
|
||||||
const {
|
const {
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
} = usePreviousHotkeyScope();
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const toggleKeyboardShortcutMenu = () => {
|
const openKeyboardShortcutMenu = useRecoilCallback(
|
||||||
if (isKeyboardShortcutMenuOpened === false) {
|
({ set }) =>
|
||||||
setIsKeyboardShortcutMenuOpened(true);
|
() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
set(isKeyboardShortcutMenuOpenedState, true);
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
);
|
AppHotkeyScope.KeyboardShortcutMenu,
|
||||||
} else {
|
);
|
||||||
setIsKeyboardShortcutMenuOpened(false);
|
},
|
||||||
goBackToPreviousHotkeyScope();
|
[setHotkeyScopeAndMemorizePreviousScope],
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const openKeyboardShortcutMenu = () => {
|
const closeKeyboardShortcutMenu = useRecoilCallback(
|
||||||
setIsKeyboardShortcutMenuOpened(true);
|
({ set, snapshot }) =>
|
||||||
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.KeyboardShortcutMenu);
|
() => {
|
||||||
};
|
const isKeyboardShortcutMenuOpened = snapshot
|
||||||
|
.getLoadable(isKeyboardShortcutMenuOpenedState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
const closeKeyboardShortcutMenu = () => {
|
if (isKeyboardShortcutMenuOpened) {
|
||||||
setIsKeyboardShortcutMenuOpened(false);
|
set(isKeyboardShortcutMenuOpenedState, false);
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[goBackToPreviousHotkeyScope],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleKeyboardShortcutMenu = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const isKeyboardShortcutMenuOpened = snapshot
|
||||||
|
.getLoadable(isKeyboardShortcutMenuOpenedState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (isKeyboardShortcutMenuOpened === false) {
|
||||||
|
openKeyboardShortcutMenu();
|
||||||
|
} else {
|
||||||
|
closeKeyboardShortcutMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[closeKeyboardShortcutMenu, openKeyboardShortcutMenu],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toggleKeyboardShortcutMenu,
|
toggleKeyboardShortcutMenu,
|
||||||
|
|||||||
@ -5,10 +5,14 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||||
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
|
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 { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
|
||||||
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
|
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
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 { 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 { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
||||||
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
||||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
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 { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
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 { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
|
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 { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
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 { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
@ -71,6 +72,9 @@ export const RecordBoard = () => {
|
|||||||
|
|
||||||
const { closeDropdown } = useDropdownV2();
|
const { closeDropdown } = useDropdownV2();
|
||||||
|
|
||||||
|
const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId);
|
||||||
|
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
|
||||||
|
|
||||||
const handleDragSelectionStart = () => {
|
const handleDragSelectionStart = () => {
|
||||||
closeDropdown(actionMenuId);
|
closeDropdown(actionMenuId);
|
||||||
|
|
||||||
@ -91,10 +95,6 @@ export const RecordBoard = () => {
|
|||||||
recordIndexRecordIdsByGroupComponentFamilyState,
|
recordIndexRecordIdsByGroupComponentFamilyState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2(
|
|
||||||
recordIndexAllRecordIdsComponentSelector,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { resetRecordSelection, setRecordAsSelected } =
|
const { resetRecordSelection, setRecordAsSelected } =
|
||||||
useRecordBoardSelection(recordBoardId);
|
useRecordBoardSelection(recordBoardId);
|
||||||
|
|
||||||
@ -115,26 +115,11 @@ export const RecordBoard = () => {
|
|||||||
refs: [],
|
refs: [],
|
||||||
callback: () => {
|
callback: () => {
|
||||||
resetRecordSelection();
|
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(
|
const setIsRemoveSortingModalOpen = useSetRecoilState(
|
||||||
isRemoveSortingModalOpenState,
|
isRemoveSortingModalOpenState,
|
||||||
);
|
);
|
||||||
@ -228,16 +213,19 @@ export const RecordBoard = () => {
|
|||||||
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
|
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
|
||||||
>
|
>
|
||||||
<RecordBoardStickyHeaderEffect />
|
<RecordBoardStickyHeaderEffect />
|
||||||
|
<RecordBoardScrollToFocusedCardEffect />
|
||||||
|
<RecordBoardDeactivateBoardCardEffect />
|
||||||
<StyledContainerContainer>
|
<StyledContainerContainer>
|
||||||
<RecordBoardHeader />
|
<RecordBoardHeader />
|
||||||
<StyledBoardContentContainer>
|
<StyledBoardContentContainer>
|
||||||
<StyledContainer ref={boardRef}>
|
<StyledContainer ref={boardRef}>
|
||||||
<DragDropContext onDragEnd={handleDragEnd}>
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<StyledColumnContainer>
|
<StyledColumnContainer>
|
||||||
{visibleRecordGroupIds.map((recordGroupId) => (
|
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||||
<RecordBoardColumn
|
<RecordBoardColumn
|
||||||
key={recordGroupId}
|
key={recordGroupId}
|
||||||
recordBoardColumnId={recordGroupId}
|
recordBoardColumnId={recordGroupId}
|
||||||
|
recordBoardColumnIndex={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledColumnContainer>
|
</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 (
|
return (
|
||||||
<StyledHeaderContainer id="record-board-header">
|
<StyledHeaderContainer id="record-board-header">
|
||||||
{visibleRecordGroupIds.map((recordGroupId) => (
|
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||||
<RecordBoardColumnHeaderWrapper
|
<RecordBoardColumnHeaderWrapper
|
||||||
columnId={recordGroupId}
|
columnId={recordGroupId}
|
||||||
|
columnIndex={index}
|
||||||
key={recordGroupId}
|
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 { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
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 { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||||
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
|
||||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
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 =
|
const isRecordBoardCardSelectedFamilyState =
|
||||||
useRecoilComponentCallbackStateV2(
|
useRecoilComponentCallbackStateV2(
|
||||||
isRecordBoardCardSelectedComponentFamilyState,
|
isRecordBoardCardSelectedComponentFamilyState,
|
||||||
@ -24,7 +31,7 @@ export const useRecordBoardSelection = (recordBoardId: string) => {
|
|||||||
const { closeDropdown } = useDropdownV2();
|
const { closeDropdown } = useDropdownV2();
|
||||||
|
|
||||||
const dropdownId = getActionMenuDropdownIdFromActionMenuId(
|
const dropdownId = getActionMenuDropdownIdFromActionMenuId(
|
||||||
getActionMenuIdFromRecordIndexId(recordBoardId),
|
getActionMenuIdFromRecordIndexId(instanceIdFromProps),
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetRecordSelection = useRecoilCallback(
|
const resetRecordSelection = useRecoilCallback(
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/get
|
|||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
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 { 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 { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||||
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
|
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 { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||||
@ -30,27 +33,44 @@ import { AnimatedEaseInOut } from 'twenty-ui/utilities';
|
|||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
|
||||||
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
const StyledBoardCard = styled.div<{
|
||||||
background-color: ${({ theme, selected }) =>
|
isDragging?: boolean;
|
||||||
selected ? theme.accent.quaternary : theme.background.secondary};
|
}>`
|
||||||
border: 1px solid
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
${({ theme, selected }) =>
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
selected ? theme.adaptiveColors.blue3 : theme.border.color.medium};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
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;
|
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 {
|
.checkbox-container {
|
||||||
transition: all ease-in-out 160ms;
|
transition: all ease-in-out 160ms;
|
||||||
opacity: ${({ selected }) => (selected ? 1 : 0)};
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected='true'] .checkbox-container {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .checkbox-container {
|
&:hover .checkbox-container {
|
||||||
@ -75,7 +95,9 @@ export const RecordBoardCard = () => {
|
|||||||
const navigate = useNavigateApp();
|
const navigate = useNavigateApp();
|
||||||
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
||||||
|
|
||||||
const { recordId } = useContext(RecordBoardCardContext);
|
const { recordId, rowIndex, columnIndex } = useContext(
|
||||||
|
RecordBoardCardContext,
|
||||||
|
);
|
||||||
|
|
||||||
const visibleFieldDefinitions = useRecoilComponentValueV2(
|
const visibleFieldDefinitions = useRecoilComponentValueV2(
|
||||||
recordBoardVisibleFieldDefinitionsComponentSelector,
|
recordBoardVisibleFieldDefinitionsComponentSelector,
|
||||||
@ -93,6 +115,22 @@ export const RecordBoardCard = () => {
|
|||||||
recordId,
|
recordId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCurrentCardFocused = useRecoilComponentFamilyValueV2(
|
||||||
|
isRecordBoardCardFocusedComponentFamilyState,
|
||||||
|
{
|
||||||
|
rowIndex,
|
||||||
|
columnIndex,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCurrentCardActive = useRecoilComponentFamilyValueV2(
|
||||||
|
isRecordBoardCardActiveComponentFamilyState,
|
||||||
|
{
|
||||||
|
rowIndex,
|
||||||
|
columnIndex,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { objectNameSingular } = useRecordIndexContextOrThrow();
|
const { objectNameSingular } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
const recordBoardId = useAvailableScopeIdOrThrow(
|
const recordBoardId = useAvailableScopeIdOrThrow(
|
||||||
@ -167,7 +205,9 @@ export const RecordBoardCard = () => {
|
|||||||
<InView>
|
<InView>
|
||||||
<StyledBoardCard
|
<StyledBoardCard
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
selected={isCurrentCardSelected}
|
data-selected={isCurrentCardSelected}
|
||||||
|
data-focused={isCurrentCardFocused}
|
||||||
|
data-active={isCurrentCardActive}
|
||||||
onMouseLeave={onMouseLeaveBoard}
|
onMouseLeave={onMouseLeaveBoard}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,25 +1,50 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
import { Draggable } from '@hello-pangea/dnd';
|
import { Draggable } from '@hello-pangea/dnd';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
|
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 { 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 { 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 = ({
|
export const RecordBoardCardDraggableContainer = ({
|
||||||
recordId,
|
recordId,
|
||||||
index,
|
rowIndex,
|
||||||
}: {
|
}: {
|
||||||
recordId: string;
|
recordId: string;
|
||||||
index: number;
|
rowIndex: number;
|
||||||
}) => {
|
}) => {
|
||||||
const isRecordReadOnly = useIsRecordReadOnly({
|
const isRecordReadOnly = useIsRecordReadOnly({
|
||||||
recordId,
|
recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { columnIndex } = useContext(RecordBoardColumnContext);
|
||||||
|
|
||||||
|
const isRecordBoardCardFocusActive = useRecoilComponentFamilyValueV2(
|
||||||
|
isRecordBoardCardFocusedComponentFamilyState,
|
||||||
|
{
|
||||||
|
rowIndex,
|
||||||
|
columnIndex,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordBoardCardContext.Provider value={{ recordId, isRecordReadOnly }}>
|
<RecordBoardCardContext.Provider
|
||||||
<Draggable key={recordId} draggableId={recordId} index={index}>
|
value={{ recordId, isRecordReadOnly, rowIndex, columnIndex }}
|
||||||
|
>
|
||||||
|
<Draggable key={recordId} draggableId={recordId} index={rowIndex}>
|
||||||
{(draggableProvided) => (
|
{(draggableProvided) => (
|
||||||
<div
|
<StyledDraggableContainer
|
||||||
|
id={`record-board-card-${columnIndex}-${rowIndex}`}
|
||||||
ref={draggableProvided?.innerRef}
|
ref={draggableProvided?.innerRef}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...draggableProvided?.dragHandleProps}
|
{...draggableProvided?.dragHandleProps}
|
||||||
@ -29,8 +54,11 @@ export const RecordBoardCardDraggableContainer = ({
|
|||||||
data-selectable-id={recordId}
|
data-selectable-id={recordId}
|
||||||
data-select-disable
|
data-select-disable
|
||||||
>
|
>
|
||||||
|
{isRecordBoardCardFocusActive && (
|
||||||
|
<RecordBoardCardFocusHotkeyEffect />
|
||||||
|
)}
|
||||||
<RecordBoardCard />
|
<RecordBoardCard />
|
||||||
</div>
|
</StyledDraggableContainer>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</RecordBoardCardContext.Provider>
|
</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 = {
|
type RecordBoardCardContextProps = {
|
||||||
recordId: string;
|
recordId: string;
|
||||||
isRecordReadOnly: boolean;
|
isRecordReadOnly: boolean;
|
||||||
|
rowIndex: number;
|
||||||
|
columnIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardCardContext =
|
export const RecordBoardCardContext =
|
||||||
|
|||||||
@ -22,10 +22,12 @@ const StyledColumn = styled.div`
|
|||||||
|
|
||||||
type RecordBoardColumnProps = {
|
type RecordBoardColumnProps = {
|
||||||
recordBoardColumnId: string;
|
recordBoardColumnId: string;
|
||||||
|
recordBoardColumnIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardColumn = ({
|
export const RecordBoardColumn = ({
|
||||||
recordBoardColumnId,
|
recordBoardColumnId,
|
||||||
|
recordBoardColumnIndex,
|
||||||
}: RecordBoardColumnProps) => {
|
}: RecordBoardColumnProps) => {
|
||||||
const recordGroupDefinition = useRecoilValue(
|
const recordGroupDefinition = useRecoilValue(
|
||||||
recordGroupDefinitionFamilyState(recordBoardColumnId),
|
recordGroupDefinitionFamilyState(recordBoardColumnId),
|
||||||
@ -46,6 +48,7 @@ export const RecordBoardColumn = ({
|
|||||||
columnDefinition: recordGroupDefinition,
|
columnDefinition: recordGroupDefinition,
|
||||||
columnId: recordBoardColumnId,
|
columnId: recordBoardColumnId,
|
||||||
recordIds: recordIdsByGroup,
|
recordIds: recordIdsByGroup,
|
||||||
|
columnIndex: recordBoardColumnIndex,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Droppable droppableId={recordBoardColumnId}>
|
<Droppable droppableId={recordBoardColumnId}>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const RecordBoardColumnCardsMemo = React.memo(
|
|||||||
<RecordBoardCardDraggableContainer
|
<RecordBoardCardDraggableContainer
|
||||||
key={recordId}
|
key={recordId}
|
||||||
recordId={recordId}
|
recordId={recordId}
|
||||||
index={index}
|
rowIndex={index}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,10 +8,12 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
|
|
||||||
type RecordBoardColumnHeaderWrapperProps = {
|
type RecordBoardColumnHeaderWrapperProps = {
|
||||||
columnId: string;
|
columnId: string;
|
||||||
|
columnIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardColumnHeaderWrapper = ({
|
export const RecordBoardColumnHeaderWrapper = ({
|
||||||
columnId,
|
columnId,
|
||||||
|
columnIndex,
|
||||||
}: RecordBoardColumnHeaderWrapperProps) => {
|
}: RecordBoardColumnHeaderWrapperProps) => {
|
||||||
const recordGroupDefinition = useRecoilValue(
|
const recordGroupDefinition = useRecoilValue(
|
||||||
recordGroupDefinitionFamilyState(columnId),
|
recordGroupDefinitionFamilyState(columnId),
|
||||||
@ -32,6 +34,7 @@ export const RecordBoardColumnHeaderWrapper = ({
|
|||||||
columnId,
|
columnId,
|
||||||
columnDefinition: recordGroupDefinition,
|
columnDefinition: recordGroupDefinition,
|
||||||
recordIds: recordIdsByGroup,
|
recordIds: recordIdsByGroup,
|
||||||
|
columnIndex,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordBoardColumnHeader />
|
<RecordBoardColumnHeader />
|
||||||
|
|||||||
@ -6,6 +6,7 @@ type RecordBoardColumnContextProps = {
|
|||||||
columnDefinition: RecordGroupDefinition;
|
columnDefinition: RecordGroupDefinition;
|
||||||
columnId: string;
|
columnId: string;
|
||||||
recordIds: string[];
|
recordIds: string[];
|
||||||
|
columnIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardColumnContext =
|
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 { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
|
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 { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
||||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||||
|
|
||||||
type RecordIndexBoardContainerProps = {
|
type RecordIndexBoardContainerProps = {
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
@ -55,6 +56,8 @@ export const RecordIndexBoardContainer = ({
|
|||||||
>
|
>
|
||||||
<RecordBoard />
|
<RecordBoard />
|
||||||
<RecordIndexRemoveSortingModal />
|
<RecordIndexRemoveSortingModal />
|
||||||
|
<RecordBoardHotkeyEffect />
|
||||||
|
<RecordBoardBodyEscapeHotkeyEffect />
|
||||||
</RecordBoardContext.Provider>
|
</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 { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
|
||||||
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
|
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 { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
|
||||||
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
@ -50,7 +51,17 @@ export const RecordTableWithWrappers = ({
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'ctrl+a,meta+a',
|
'ctrl+a,meta+a',
|
||||||
handleSelectAllRows,
|
handleSelectAllRows,
|
||||||
TableHotkeyScope.Table,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
enableOnFormTags: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'ctrl+a,meta+a',
|
||||||
|
handleSelectAllRows,
|
||||||
|
TableHotkeyScope.TableFocus,
|
||||||
[],
|
[],
|
||||||
{
|
{
|
||||||
enableOnFormTags: false,
|
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 { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||||
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
import { useSetIsRecordTableFocusActive } from '@/object-record/record-table/record-table-cell/hooks/useSetIsRecordTableFocusActive';
|
||||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
@ -49,6 +49,6 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
|
|||||||
|
|
||||||
setRecordTableHoverPosition(null);
|
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
|
||||||
import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
|
import { useUpsertRecordFromState } from '../../hooks/useUpsertRecordFromState';
|
||||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||||
@ -168,7 +169,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||||
move('up');
|
move('up');
|
||||||
},
|
},
|
||||||
TableHotkeyScope.Table,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
[move],
|
[move],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||||
move('down');
|
move('down');
|
||||||
},
|
},
|
||||||
TableHotkeyScope.Table,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
[move],
|
[move],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { Key } from 'ts-key-enum';
|
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 { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
|
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
export const RecordTableBodyEscapeHotkeyEffect = () => {
|
export const RecordTableBodyEscapeHotkeyEffect = () => {
|
||||||
const { recordTableId } = useRecordTableContextOrThrow();
|
const { recordTableId } = useRecordTableContextOrThrow();
|
||||||
|
|
||||||
@ -28,9 +29,9 @@ export const RecordTableBodyEscapeHotkeyEffect = () => {
|
|||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TableHotkeyScope.Table,
|
RecordIndexHotkeyScope.RecordIndex,
|
||||||
[isAtLeastOneRecordSelected, resetTableRowSelection, unfocusRecordTableRow],
|
[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 { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
@ -37,7 +38,7 @@ export const RecordTableBodyFocusKeyboardEffect = () => {
|
|||||||
restoreRecordTableRowFocusFromCellPosition();
|
restoreRecordTableRowFocusFromCellPosition();
|
||||||
setIsFocusActiveForCurrentPosition(false);
|
setIsFocusActiveForCurrentPosition(false);
|
||||||
} else {
|
} else {
|
||||||
setHotkeyScope(TableHotkeyScope.Table, {
|
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: 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 { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
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 { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||||
import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector';
|
import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector';
|
||||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
@ -38,8 +39,8 @@ export const useMoveHoverToCurrentCell = (recordTableId: string) => {
|
|||||||
if (
|
if (
|
||||||
currentHotkeyScope.scope !== TableHotkeyScope.TableFocus &&
|
currentHotkeyScope.scope !== TableHotkeyScope.TableFocus &&
|
||||||
currentHotkeyScope.scope !== TableHotkeyScope.CellEditMode &&
|
currentHotkeyScope.scope !== TableHotkeyScope.CellEditMode &&
|
||||||
currentHotkeyScope.scope !== TableHotkeyScope.Table &&
|
currentHotkeyScope.scope !== AppHotkeyScope.CommandMenuOpen &&
|
||||||
currentHotkeyScope.scope !== AppHotkeyScope.CommandMenuOpen
|
currentHotkeyScope.scope !== RecordIndexHotkeyScope.RecordIndex
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,13 +139,9 @@ export const RecordTableTr = forwardRef<
|
|||||||
data-virtualized-id={recordId}
|
data-virtualized-id={recordId}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-active={isActive ? 'true' : 'false'}
|
data-active={isActive}
|
||||||
data-focused={
|
data-focused={isRowFocusActive && isFocused && !isActive}
|
||||||
isRowFocusActive && isFocused && !isActive ? 'true' : 'false'
|
data-next-row-active-or-focused={isNextRowActiveOrFocused}
|
||||||
}
|
|
||||||
data-next-row-active-or-focused={
|
|
||||||
isNextRowActiveOrFocused ? 'true' : 'false'
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -3,5 +3,4 @@ export enum TableHotkeyScope {
|
|||||||
CellEditMode = 'cell-edit-mode',
|
CellEditMode = 'cell-edit-mode',
|
||||||
CellDateEditMode = 'cell-date-edit-mode',
|
CellDateEditMode = 'cell-date-edit-mode',
|
||||||
TableFocus = 'table-focus',
|
TableFocus = 'table-focus',
|
||||||
Table = 'table',
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user