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

View File

@ -3,137 +3,30 @@ 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 { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys';
import { RECORD_INDEX_FOCUS_ID } from '@/object-record/record-index/constants/RecordIndexFocusId';
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';
const BOARD_NAVIGATION_CUSTOM_SCOPES = {
goto: true,
keyboardShortcutMenu: true,
searchRecords: true,
};
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
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({
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,
});
useHotkeysOnFocusedElement({
keys: [Key.ArrowLeft, Key.ArrowUp, Key.ArrowDown, Key.ArrowRight],
callback: () => {
move('down');
},
RecordIndexHotkeyScope.RecordIndex,
);
focusId: RECORD_INDEX_FOCUS_ID,
scope: RecordIndexHotkeyScope.RecordIndex,
dependencies: [move],
});
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,
);
useRecordBoardSelectAllHotkeys({
recordBoardId,
focusId: recordBoardId,
});
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 { 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 { 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 { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -22,6 +27,10 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
recordBoardId,
);
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const unfocusBoardCard = useRecoilCallback(
({ set, snapshot }) =>
() => {
@ -33,11 +42,26 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
return;
}
const focusId = getRecordBoardCardFocusId({
recordBoardId: recordBoardId || '',
cardIndexes: focusedBoardCardIndexes,
});
removeFocusItemFromFocusStackById({
focusId,
});
set(focusedBoardCardIndexesState, null);
set(isCardFocusedState(focusedBoardCardIndexes), false);
set(isCardFocusActiveState, false);
},
[focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState],
[
focusedBoardCardIndexesState,
isCardFocusedState,
isCardFocusActiveState,
recordBoardId,
removeFocusItemFromFocusStackById,
],
);
const focusBoardCard = useRecoilCallback(
@ -54,13 +78,51 @@ export const useFocusedRecordBoardCard = (recordBoardId?: string) => {
boardCardIndexes.columnIndex)
) {
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(isCardFocusedState(boardCardIndexes), true);
set(isCardFocusActiveState, true);
},
[focusedBoardCardIndexesState, isCardFocusedState, isCardFocusActiveState],
[
focusedBoardCardIndexesState,
isCardFocusedState,
isCardFocusActiveState,
recordBoardId,
pushFocusItemToFocusStack,
removeFocusItemFromFocusStackById,
],
);
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 { 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 { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
@ -57,9 +57,7 @@ export const RecordBoardCardDraggableContainer = ({
data-selectable-id={recordId}
data-select-disable
>
{isRecordBoardCardFocusActive && (
<RecordBoardCardFocusHotkeyEffect />
)}
{isRecordBoardCardFocusActive && <RecordBoardCardHotkeysEffect />}
<RecordBoardCard />
</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 { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
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 {
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 { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
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 { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -75,7 +75,7 @@ export const useOpenFieldInputEditMode = () => {
});
openActivityTargetCellEditMode({
recordPickerInstanceId: getRelationFromManyFieldInputInstanceId({
recordPickerInstanceId: getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
@ -110,16 +110,16 @@ export const useOpenFieldInputEditMode = () => {
}
pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId(
focusId: getFieldInputInstanceId({
recordId,
fieldDefinition.metadata.fieldName,
),
fieldName: fieldDefinition.metadata.fieldName,
}),
component: {
type: FocusComponentType.OPEN_FIELD_INPUT,
instanceId: getFieldInputInstanceId(
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getFieldInputInstanceId({
recordId,
fieldDefinition.metadata.fieldName,
),
fieldName: fieldDefinition.metadata.fieldName,
}),
},
hotkeyScope: {
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 {
openFieldInput: openFieldInput,
closeFieldInput: () => {},
openFieldInput,
closeFieldInput,
};
};

View File

@ -18,10 +18,10 @@ export const MultiSelectFieldInput = ({
selectableListComponentInstanceId={
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
focusId={getFieldInputInstanceId(
focusId={getFieldInputInstanceId({
recordId,
fieldDefinition.metadata.fieldName,
)}
fieldName: fieldDefinition.metadata.fieldName,
})}
options={fieldDefinition.metadata.options}
onCancel={onCancel}
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 { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
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 { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
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 { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
@ -31,7 +31,7 @@ export const RelationFromManyFieldInput = ({
onSubmit,
}: RelationFromManyFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const recordPickerInstanceId = getRelationFromManyFieldInputInstanceId({
const recordPickerInstanceId = getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});

View File

@ -3,10 +3,10 @@ import { useRelationField } from '../../hooks/useRelationField';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
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 { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
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 { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
@ -29,7 +29,7 @@ export const RelationToOneFieldInput = ({
const persistField = usePersistField();
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({
const recordPickerInstanceId = getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});

View File

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

View File

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

View File

@ -16,9 +16,9 @@ import {
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
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 { 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 { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -68,13 +68,13 @@ const RelationToOneFieldInputWithContext = ({
useEffect(() => {
pushFocusItemToFocusStack({
focusId: getRelationToOneFieldInputInstanceId({
focusId: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
component: {
type: FocusComponentType.DROPDOWN,
instanceId: getRelationToOneFieldInputInstanceId({
instanceId: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
@ -82,7 +82,7 @@ const RelationToOneFieldInputWithContext = ({
hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown,
},
memoizeKey: getRelationToOneFieldInputInstanceId({
memoizeKey: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),

View File

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

View File

@ -1,8 +1,8 @@
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import {
FieldRelationToOneValue,
FieldRelationValue,
} 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 { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
@ -19,7 +19,7 @@ export const useOpenRelationToOneFieldInput = () => {
const openRelationToOneFieldInput = useRecoilCallback(
({ set, snapshot }) =>
({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({
const recordPickerInstanceId = getFieldInputInstanceId({
recordId,
fieldName,
});
@ -46,7 +46,7 @@ export const useOpenRelationToOneFieldInput = () => {
pushFocusItemToFocusStack({
focusId: recordPickerInstanceId,
component: {
type: FocusComponentType.OPEN_FIELD_INPUT,
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: recordPickerInstanceId,
},
// 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 = (
recordId: string,
fieldName: string,
) => {
export const getFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}) => {
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 { FieldInput } from '@/object-record/record-field/components/FieldInput';
@ -13,10 +13,13 @@ import {
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
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 { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
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 { RecordInlineCellContainer } from './RecordInlineCellContainer';
import {
@ -35,16 +38,47 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
recordId,
isCentered,
isDisplayModeFixHeight,
onOpenEditMode,
onCloseEditMode,
onOpenEditMode: onOpenEditModeFromContext,
onCloseEditMode: onCloseEditModeFromContext,
isReadOnly,
} = 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 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) => {
persistField();
@ -94,7 +128,6 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
);
const { getIcon } = useIcons();
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
readonly: isReadOnly,
@ -122,9 +155,8 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
isDisplayModeFixHeight: isDisplayModeFixHeight,
editModeContentOnly: isFieldInputOnly,
loading: loading,
onOpenEditMode:
onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })),
onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()),
onOpenEditMode,
onCloseEditMode,
};
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 { 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 {
AppTooltip,
@ -160,6 +161,9 @@ export const RecordInlineCellContainer = () => {
)}
</StyledLabelAndIconContainer>
)}
{isInlineCellInEditMode && (
<RecordInlineCellCloseOnCommandMenuOpeningEffect />
)}
<StyledValueContainer readonly={readonly ?? false}>
<RecordInlineCellValue />
</StyledValueContainer>

View File

@ -2,12 +2,10 @@ import { useContext } from 'react';
import { useRecoilState } from 'recoil';
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 { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
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 { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
@ -35,16 +33,12 @@ export const useInlineCell = (
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const initFieldInputDraftValue = useInitDraftValueV2();
const closeInlineCell = () => {
onCloseEditMode?.();
setIsInlineCellInEditMode(false);
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
goBackToPreviousDropdownFocusId();
};

View File

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

View File

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