Replace hotkey scopes by focus stack (Part 3 - Record Board, Cards and Inline Cells) (#12910)

# Replace hotkey scopes by focus stack (Part 3 - Record Board, Cards and
Inline Cells)

This PR is the second part of a refactoring aiming to deprecate the
hotkey scopes api in favor of the new focus stack api which is more
robust.
Part 1: https://github.com/twentyhq/twenty/pull/12673
Part 2: https://github.com/twentyhq/twenty/pull/12798

The board shortcuts are no longer centralized in the record board, they
are now split and the focused element is in charge of applying the
desired shortcuts.

## Video QA:


https://github.com/user-attachments/assets/20ba4a24-6fc3-4a97-9cd3-68e846699e30
This commit is contained in:
Raphaël Bosi
2025-06-26 18:07:54 +02:00
committed by GitHub
parent 12add0e1f9
commit 3241539db9
29 changed files with 436 additions and 281 deletions

View File

@ -5,17 +5,17 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; 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 { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { BoardHotkeyScope } from '@/object-record/record-board/types/BoardHotkeyScope'; import { useResetFocusStackToRecordIndex } from '@/object-record/record-index/hooks/useResetFocusStackToRecordIndex';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordBoardBodyEscapeHotkeyEffect = () => { export const RecordBoardBodyEscapeHotkeyEffect = () => {
const { recordBoardId } = useContext(RecordBoardContext); const { recordBoardId } = useContext(RecordBoardContext);
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex();
const selectedRecordIds = useRecoilComponentValueV2( const selectedRecordIds = useRecoilComponentValueV2(
recordBoardSelectedRecordIdsComponentSelector, recordBoardSelectedRecordIdsComponentSelector,
@ -24,29 +24,21 @@ export const RecordBoardBodyEscapeHotkeyEffect = () => {
const isAtLeastOneRecordSelected = selectedRecordIds.length > 0; const isAtLeastOneRecordSelected = selectedRecordIds.length > 0;
useScopedHotkeys( const handleEscape = () => {
[Key.Escape], unfocusBoardCard();
() => { if (isAtLeastOneRecordSelected) {
unfocusBoardCard(); resetRecordSelection();
if (isAtLeastOneRecordSelected) { }
resetRecordSelection(); resetFocusStackToRecordIndex();
} };
},
RecordIndexHotkeyScope.RecordIndex,
[isAtLeastOneRecordSelected, resetRecordSelection, unfocusBoardCard],
);
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Escape], keys: [Key.Escape],
() => { callback: handleEscape,
unfocusBoardCard(); focusId: recordBoardId,
if (isAtLeastOneRecordSelected) { scope: RecordIndexHotkeyScope.RecordIndex,
resetRecordSelection(); dependencies: [handleEscape],
} });
},
BoardHotkeyScope.BoardFocus,
[isAtLeastOneRecordSelected, resetRecordSelection, unfocusBoardCard],
);
return null; return null;
}; };

View File

@ -3,137 +3,30 @@ import { Key } from 'ts-key-enum';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation'; import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys';
import { BoardHotkeyScope } from '@/object-record/record-board/types/BoardHotkeyScope'; import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope'; import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
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';
const BOARD_NAVIGATION_CUSTOM_SCOPES = {
goto: true,
keyboardShortcutMenu: true,
searchRecords: true,
};
export const RecordBoardHotkeyEffect = () => { export const RecordBoardHotkeyEffect = () => {
const { recordBoardId } = useContext(RecordBoardContext); const { recordBoardId } = useContext(RecordBoardContext);
const { move } = useRecordBoardCardNavigation(recordBoardId); const { move } = useRecordBoardCardNavigation(recordBoardId);
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); useHotkeysOnFocusedElement({
keys: [Key.ArrowLeft, Key.ArrowUp, Key.ArrowDown, Key.ArrowRight],
const recordIndexAllRecordIdsState = useRecoilComponentCallbackStateV2( callback: () => {
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({
scope: BoardHotkeyScope.BoardFocus,
customScopes: BOARD_NAVIGATION_CUSTOM_SCOPES,
});
move('left');
},
RecordIndexHotkeyScope.RecordIndex,
);
useScopedHotkeys(
Key.ArrowRight,
() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: BoardHotkeyScope.BoardFocus,
customScopes: BOARD_NAVIGATION_CUSTOM_SCOPES,
});
move('right');
},
RecordIndexHotkeyScope.RecordIndex,
);
useScopedHotkeys(
Key.ArrowUp,
() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: BoardHotkeyScope.BoardFocus,
customScopes: BOARD_NAVIGATION_CUSTOM_SCOPES,
});
move('up');
},
RecordIndexHotkeyScope.RecordIndex,
);
useScopedHotkeys(
Key.ArrowDown,
() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: BoardHotkeyScope.BoardFocus,
customScopes: BOARD_NAVIGATION_CUSTOM_SCOPES,
});
move('down'); move('down');
}, },
RecordIndexHotkeyScope.RecordIndex, focusId: RECORD_INDEX_FOCUS_ID,
); scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
useScopedHotkeys( useRecordBoardSelectAllHotkeys({
Key.ArrowLeft, recordBoardId,
() => { focusId: recordBoardId,
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; return null;
}; };

View File

@ -0,0 +1 @@
export const RECORD_BOARD_FOCUS_ID = 'record-board';

View File

@ -1,7 +1,12 @@
import { getRecordBoardCardFocusId } from '@/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId';
import { focusedRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState'; import { focusedRecordBoardCardIndexesComponentState } from '@/object-record/record-board/states/focusedRecordBoardCardIndexesComponentState';
import { isRecordBoardCardFocusActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState'; import { isRecordBoardCardFocusActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCardFocusActiveComponentState';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState'; import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes'; import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -22,6 +27,10 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
recordBoardId, recordBoardId,
); );
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const unfocusBoardCard = useRecoilCallback( const unfocusBoardCard = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
() => { () => {
@ -33,11 +42,26 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
return; return;
} }
const focusId = getRecordBoardCardFocusId({
recordBoardId: recordBoardId || '',
cardIndexes: focusedBoardCardIndexes,
});
removeFocusItemFromFocusStackById({
focusId,
});
set(focusedBoardCardIndexesState, null); set(focusedBoardCardIndexesState, null);
set(isCardFocusedState(focusedBoardCardIndexes), false); set(isCardFocusedState(focusedBoardCardIndexes), false);
set(isCardFocusActiveState, false); set(isCardFocusActiveState, false);
}, },
[focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState], [
focusedBoardCardIndexesState,
isCardFocusedState,
isCardFocusActiveState,
recordBoardId,
removeFocusItemFromFocusStackById,
],
); );
const focusBoardCard = useRecoilCallback( const focusBoardCard = useRecoilCallback(
@ -54,13 +78,51 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
boardCardIndexes.columnIndex) boardCardIndexes.columnIndex)
) { ) {
set(isCardFocusedState(focusedBoardCardIndexes), false); set(isCardFocusedState(focusedBoardCardIndexes), false);
const currentFocusId = getRecordBoardCardFocusId({
recordBoardId: recordBoardId || '',
cardIndexes: focusedBoardCardIndexes,
});
removeFocusItemFromFocusStackById({
focusId: currentFocusId,
});
} }
const focusId = getRecordBoardCardFocusId({
recordBoardId: recordBoardId || '',
cardIndexes: boardCardIndexes,
});
pushFocusItemToFocusStack({
focusId,
component: {
type: FocusComponentType.RECORD_BOARD_CARD,
instanceId: focusId,
},
hotkeyScope: {
scope: RecordIndexHotkeyScope.RecordIndex,
customScopes: {
goto: true,
keyboardShortcutMenu: true,
searchRecords: true,
},
},
memoizeKey: focusId,
});
set(focusedBoardCardIndexesState, boardCardIndexes); set(focusedBoardCardIndexesState, boardCardIndexes);
set(isCardFocusedState(boardCardIndexes), true); set(isCardFocusedState(boardCardIndexes), true);
set(isCardFocusActiveState, true); set(isCardFocusActiveState, true);
}, },
[focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState], [
focusedBoardCardIndexesState,
isCardFocusedState,
isCardFocusActiveState,
recordBoardId,
pushFocusItemToFocusStack,
removeFocusItemFromFocusStackById,
],
); );
return { return {

View File

@ -0,0 +1,46 @@
import { useRecordBoardCardNavigation } from '@/object-record/record-board/hooks/useRecordBoardCardNavigation';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { Key } from 'ts-key-enum';
export const useRecordBoardArrowKeysEffect = ({
recordBoardId,
focusId,
}: {
recordBoardId: string;
focusId: string;
}) => {
const { move } = useRecordBoardCardNavigation(recordBoardId);
useHotkeysOnFocusedElement({
keys: [Key.ArrowLeft],
callback: () => move('left'),
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
useHotkeysOnFocusedElement({
keys: [Key.ArrowRight],
callback: () => move('right'),
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
useHotkeysOnFocusedElement({
keys: [Key.ArrowUp],
callback: () => move('up'),
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
useHotkeysOnFocusedElement({
keys: [Key.ArrowDown],
callback: () => move('down'),
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
};

View File

@ -0,0 +1,91 @@
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 { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
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 { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useContext } from 'react';
import { Key } from 'ts-key-enum';
export const useRecordBoardCardHotkeys = (focusId: string) => {
const { objectMetadataItem, recordBoardId } = useContext(RecordBoardContext);
const { recordId, rowIndex, columnIndex } = useContext(
RecordBoardCardContext,
);
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { activateBoardCard } = useActiveRecordBoardCard();
const { setRecordAsSelected, resetRecordSelection } =
useRecordBoardSelection();
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId);
const isRecordBoardCardSelected = useRecoilComponentFamilyValueV2(
isRecordBoardCardSelectedComponentFamilyState,
recordId,
);
const selectedRecordIds = useRecoilComponentValueV2(
recordBoardSelectedRecordIdsComponentSelector,
recordBoardId,
);
const isAtLeastOneRecordSelected = selectedRecordIds.length > 0;
const handleSelectCard = () => {
setRecordAsSelected(recordId, !isRecordBoardCardSelected);
};
const handleOpenRecordInCommandMenu = () => {
openRecordInCommandMenu({
recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isNewRecord: false,
});
activateBoardCard({
rowIndex,
columnIndex,
});
};
const handleEscape = () => {
unfocusBoardCard();
if (isAtLeastOneRecordSelected) {
resetRecordSelection();
}
};
useHotkeysOnFocusedElement({
keys: ['x'],
callback: handleSelectCard,
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [handleSelectCard],
});
useHotkeysOnFocusedElement({
keys: [
Key.Enter,
`${Key.Control}+${Key.Enter}`,
`${Key.Meta}+${Key.Enter}`,
],
callback: handleOpenRecordInCommandMenu,
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [handleOpenRecordInCommandMenu],
});
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handleEscape,
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [handleEscape],
});
};

View File

@ -0,0 +1,47 @@
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
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 useRecordBoardSelectAllHotkeys = ({
recordBoardId,
focusId,
}: {
recordBoardId: string;
focusId: string;
}) => {
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],
);
useHotkeysOnFocusedElement({
keys: ['ctrl+a', 'meta+a'],
callback: selectAll,
focusId,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [selectAll],
options: {
enableOnFormTags: false,
},
});
};

View File

@ -4,7 +4,7 @@ import { useContext } from 'react';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
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 { RecordBoardCardHotkeysEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect';
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 { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState'; import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
@ -57,9 +57,7 @@ export const RecordBoardCardDraggableContainer = ({
data-selectable-id={recordId} data-selectable-id={recordId}
data-select-disable data-select-disable
> >
{isRecordBoardCardFocusActive && ( {isRecordBoardCardFocusActive && <RecordBoardCardHotkeysEffect />}
<RecordBoardCardFocusHotkeyEffect />
)}
<RecordBoardCard /> <RecordBoardCard />
</StyledDraggableContainer> </StyledDraggableContainer>
)} )}

View File

@ -1,57 +0,0 @@
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;
};

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardArrowKeysEffect } from '@/object-record/record-board/hooks/useRecordBoardArrowKeysEffect';
import { useRecordBoardCardHotkeys } from '@/object-record/record-board/hooks/useRecordBoardCardHotkeys';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { getRecordBoardCardFocusId } from '@/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId';
export const RecordBoardCardHotkeysEffect = () => {
const { recordBoardId } = useContext(RecordBoardContext);
const { rowIndex, columnIndex } = useContext(RecordBoardCardContext);
const focusId = getRecordBoardCardFocusId({
recordBoardId,
cardIndexes: { rowIndex, columnIndex },
});
useRecordBoardCardHotkeys(focusId);
useRecordBoardArrowKeysEffect({ recordBoardId, focusId });
return null;
};

View File

@ -0,0 +1,11 @@
import { BoardCardIndexes } from '@/object-record/record-board/types/BoardCardIndexes';
export const getRecordBoardCardFocusId = ({
recordBoardId,
cardIndexes,
}: {
recordBoardId: string;
cardIndexes: BoardCardIndexes;
}) => {
return `${recordBoardId}-board-card-${cardIndexes.columnIndex}-${cardIndexes.rowIndex}`;
};

View File

@ -7,7 +7,6 @@ import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTa
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput'; import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput'; import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { import {
FieldMetadata, FieldMetadata,
@ -22,6 +21,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -75,7 +75,7 @@ export const useOpenFieldInputEditMode = () => {
}); });
openActivityTargetCellEditMode({ openActivityTargetCellEditMode({
recordPickerInstanceId: getRelationFromManyFieldInputInstanceId({ recordPickerInstanceId: getFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
}), }),
@ -110,16 +110,16 @@ export const useOpenFieldInputEditMode = () => {
} }
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId( focusId: getFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
), }),
component: { component: {
type: FocusComponentType.OPEN_FIELD_INPUT, type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getFieldInputInstanceId( instanceId: getFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
), }),
}, },
hotkeyScope: { hotkeyScope: {
scope: DEFAULT_CELL_SCOPE.scope, scope: DEFAULT_CELL_SCOPE.scope,
@ -136,8 +136,26 @@ export const useOpenFieldInputEditMode = () => {
], ],
); );
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeFieldInput = ({
fieldDefinition,
recordId,
}: {
fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string;
}) => {
removeFocusItemFromFocusStackById({
focusId: getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
});
};
return { return {
openFieldInput: openFieldInput, openFieldInput,
closeFieldInput: () => {}, closeFieldInput,
}; };
}; };

View File

@ -18,10 +18,10 @@ export const MultiSelectFieldInput = ({
selectableListComponentInstanceId={ selectableListComponentInstanceId={
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
} }
focusId={getFieldInputInstanceId( focusId={getFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
)} })}
options={fieldDefinition.metadata.options} options={fieldDefinition.metadata.options}
onCancel={onCancel} onCancel={onCancel}
onOptionSelected={persistField} onOptionSelected={persistField}

View File

@ -10,11 +10,11 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField'; import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
@ -31,7 +31,7 @@ export const RelationFromManyFieldInput = ({
onSubmit, onSubmit,
}: RelationFromManyFieldInputProps) => { }: RelationFromManyFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext); const { fieldDefinition, recordId } = useContext(FieldContext);
const recordPickerInstanceId = getRelationFromManyFieldInputInstanceId({ const recordPickerInstanceId = getFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
}); });

View File

@ -3,10 +3,10 @@ import { useRelationField } from '../../hooks/useRelationField';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
@ -29,7 +29,7 @@ export const RelationToOneFieldInput = ({
const persistField = usePersistField(); const persistField = usePersistField();
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({ const recordPickerInstanceId = getFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
}); });

View File

@ -67,10 +67,10 @@ export const SelectFieldInput = ({
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
} }
selectableItemIdArray={optionIds} selectableItemIdArray={optionIds}
focusId={getFieldInputInstanceId( focusId={getFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
)} })}
onEnter={(itemId) => { onEnter={(itemId) => {
const option = filteredOptions.find( const option = filteredOptions.find(
(option) => option.value === itemId, (option) => option.value === itemId,

View File

@ -18,8 +18,8 @@ import {
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
@ -88,7 +88,7 @@ const RelationManyFieldInputWithContext = () => {
<div> <div>
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRelationFromManyFieldInputInstanceId({ instanceId: getFieldInputInstanceId({
recordId: 'recordId', recordId: 'recordId',
fieldName: 'people', fieldName: 'people',
}), }),

View File

@ -16,9 +16,9 @@ import {
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -68,13 +68,13 @@ const RelationToOneFieldInputWithContext = ({
useEffect(() => { useEffect(() => {
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getRelationToOneFieldInputInstanceId({ focusId: getFieldInputInstanceId({
recordId: '123', recordId: '123',
fieldName: 'Relation', fieldName: 'Relation',
}), }),
component: { component: {
type: FocusComponentType.DROPDOWN, type: FocusComponentType.DROPDOWN,
instanceId: getRelationToOneFieldInputInstanceId({ instanceId: getFieldInputInstanceId({
recordId: '123', recordId: '123',
fieldName: 'Relation', fieldName: 'Relation',
}), }),
@ -82,7 +82,7 @@ const RelationToOneFieldInputWithContext = ({
hotkeyScope: { hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown, scope: DropdownHotkeyScope.Dropdown,
}, },
memoizeKey: getRelationToOneFieldInputInstanceId({ memoizeKey: getFieldInputInstanceId({
recordId: '123', recordId: '123',
fieldName: 'Relation', fieldName: 'Relation',
}), }),

View File

@ -1,9 +1,9 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { import {
FieldRelationFromManyValue, FieldRelationFromManyValue,
FieldRelationValue, FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
@ -33,7 +33,7 @@ export const useOpenRelationFromManyFieldInput = () => {
objectNameSingular: string; objectNameSingular: string;
recordId: string; recordId: string;
}) => { }) => {
const recordPickerInstanceId = getRelationFromManyFieldInputInstanceId({ const recordPickerInstanceId = getFieldInputInstanceId({
recordId, recordId,
fieldName, fieldName,
}); });

View File

@ -1,8 +1,8 @@
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import { import {
FieldRelationToOneValue, FieldRelationToOneValue,
FieldRelationValue, FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
@ -19,7 +19,7 @@ export const useOpenRelationToOneFieldInput = () => {
const openRelationToOneFieldInput = useRecoilCallback( const openRelationToOneFieldInput = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
({ fieldName, recordId }: { fieldName: string; recordId: string }) => { ({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({ const recordPickerInstanceId = getFieldInputInstanceId({
recordId, recordId,
fieldName, fieldName,
}); });
@ -46,7 +46,7 @@ export const useOpenRelationToOneFieldInput = () => {
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: recordPickerInstanceId, focusId: recordPickerInstanceId,
component: { component: {
type: FocusComponentType.OPEN_FIELD_INPUT, type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: recordPickerInstanceId, instanceId: recordPickerInstanceId,
}, },
// TODO: Remove this once we've fully migrated away from hotkey scopes // TODO: Remove this once we've fully migrated away from hotkey scopes

View File

@ -1,9 +0,0 @@
export const getRelationFromManyFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}): string => {
return `relation-from-many-field-input-${recordId}-${fieldName}`;
};

View File

@ -1,9 +0,0 @@
export const getRelationToOneFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}): string => {
return `relation-to-one-field-input-${recordId}-${fieldName}`;
};

View File

@ -1,6 +1,9 @@
export const getFieldInputInstanceId = ( export const getFieldInputInstanceId = ({
recordId: string, recordId,
fieldName: string, fieldName,
) => { }: {
recordId: string;
fieldName: string;
}) => {
return `${recordId}-${fieldName}`; return `${recordId}-${fieldName}`;
}; };

View File

@ -1,4 +1,4 @@
import { useContext } from 'react'; import { useCallback, useContext } from 'react';
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay'; import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
import { FieldInput } from '@/object-record/record-field/components/FieldInput'; import { FieldInput } from '@/object-record/record-field/components/FieldInput';
@ -13,10 +13,13 @@ import {
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useRecoilCallback } from 'recoil'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui/display'; import { useIcons } from 'twenty-ui/display';
import { RecordInlineCellContainer } from './RecordInlineCellContainer'; import { RecordInlineCellContainer } from './RecordInlineCellContainer';
import { import {
@ -35,16 +38,47 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
recordId, recordId,
isCentered, isCentered,
isDisplayModeFixHeight, isDisplayModeFixHeight,
onOpenEditMode, onOpenEditMode: onOpenEditModeFromContext,
onCloseEditMode, onCloseEditMode: onCloseEditModeFromContext,
isReadOnly, isReadOnly,
} = useContext(FieldContext); } = useContext(FieldContext);
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
const onOpenEditMode = onOpenEditModeFromContext
? onOpenEditModeFromContext
: () => openFieldInput({ fieldDefinition, recordId });
const onCloseEditMode = useCallback(() => {
onCloseEditModeFromContext
? onCloseEditModeFromContext()
: closeFieldInput({ fieldDefinition, recordId });
}, [onCloseEditModeFromContext, closeFieldInput, fieldDefinition, recordId]);
const buttonIcon = useGetButtonIcon(); const buttonIcon = useGetButtonIcon();
const isFieldInputOnly = useIsFieldInputOnly(); const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell(); const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const setIsInlineCellInEditMode = useSetRecoilState(
isInlineCellInEditModeScopedState(recordFieldComponentInstanceId),
);
const closeInlineCell = useCallback(() => {
onCloseEditMode();
setIsInlineCellInEditMode(false);
goBackToPreviousDropdownFocusId();
}, [
onCloseEditMode,
setIsInlineCellInEditMode,
goBackToPreviousDropdownFocusId,
]);
const handleEnter: FieldInputEvent = (persistField) => { const handleEnter: FieldInputEvent = (persistField) => {
persistField(); persistField();
@ -94,7 +128,6 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
); );
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
const RecordInlineCellContextValue: RecordInlineCellContextProps = { const RecordInlineCellContextValue: RecordInlineCellContextProps = {
readonly: isReadOnly, readonly: isReadOnly,
@ -122,9 +155,8 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
isDisplayModeFixHeight: isDisplayModeFixHeight, isDisplayModeFixHeight: isDisplayModeFixHeight,
editModeContentOnly: isFieldInputOnly, editModeContentOnly: isFieldInputOnly,
loading: loading, loading: loading,
onOpenEditMode: onOpenEditMode,
onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })), onCloseEditMode,
onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()),
}; };
return ( return (

View File

@ -0,0 +1,14 @@
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { useListenToSidePanelOpening } from '@/ui/layout/right-drawer/hooks/useListenToSidePanelOpening';
// TODO: This is a temporary solution to close the inline cell when the command menu is opened.
// This is because the useInlineCell hook doesn't work correctly for field inputs when used outside of the field context.
// We should refactor field inputs, and remove this listener afterwards.
// TODO: create a new hook useCloseAnyOpenedFieldInput which uses the focus stack to close all the field inputs, and call it inside openCommandMenu
export const RecordInlineCellCloseOnCommandMenuOpeningEffect = () => {
const { closeInlineCell } = useInlineCell();
useListenToSidePanelOpening(closeInlineCell);
return null;
};

View File

@ -9,6 +9,7 @@ import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInput
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { RecordInlineCellCloseOnCommandMenuOpeningEffect } from '@/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { import {
AppTooltip, AppTooltip,
@ -160,6 +161,9 @@ export const RecordInlineCellContainer = () => {
)} )}
</StyledLabelAndIconContainer> </StyledLabelAndIconContainer>
)} )}
{isInlineCellInEditMode && (
<RecordInlineCellCloseOnCommandMenuOpeningEffect />
)}
<StyledValueContainer readonly={readonly ?? false}> <StyledValueContainer readonly={readonly ?? false}>
<RecordInlineCellValue /> <RecordInlineCellValue />
</StyledValueContainer> </StyledValueContainer>

View File

@ -2,12 +2,10 @@ import { useContext } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2'; import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
@ -35,16 +33,12 @@ export const useInlineCell = (
const { goBackToPreviousDropdownFocusId } = const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId(); useGoBackToPreviousDropdownFocusId();
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const initFieldInputDraftValue = useInitDraftValueV2(); const initFieldInputDraftValue = useInitDraftValueV2();
const closeInlineCell = () => { const closeInlineCell = () => {
onCloseEditMode?.(); onCloseEditMode?.();
setIsInlineCellInEditMode(false); setIsInlineCellInEditMode(false);
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
goBackToPreviousDropdownFocusId(); goBackToPreviousDropdownFocusId();
}; };

View File

@ -27,7 +27,7 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
goBackToPreviousDropdownFocusId(); goBackToPreviousDropdownFocusId();
removeLastFocusItemFromFocusStackByComponentType({ removeLastFocusItemFromFocusStackByComponentType({
componentType: FocusComponentType.OPEN_FIELD_INPUT, componentType: FocusComponentType.OPENED_FIELD_INPUT,
}); });
}; };
}, },

View File

@ -2,9 +2,11 @@ export enum FocusComponentType {
MODAL = 'modal', MODAL = 'modal',
DROPDOWN = 'dropdown', DROPDOWN = 'dropdown',
SIDE_PANEL = 'side-panel', SIDE_PANEL = 'side-panel',
OPEN_FIELD_INPUT = 'open-field-input', OPENED_FIELD_INPUT = 'opened-field-input',
PAGE = 'page', PAGE = 'page',
RECORD_TABLE = 'record-table', RECORD_TABLE = 'record-table',
RECORD_TABLE_ROW = 'record-table-row', RECORD_TABLE_ROW = 'record-table-row',
RECORD_TABLE_CELL = 'record-table-cell', RECORD_TABLE_CELL = 'record-table-cell',
RECORD_BOARD = 'record-board',
RECORD_BOARD_CARD = 'record-board-card',
} }