From 3241539db9908d82eb3d9f325b74ebc2b1691ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:07:54 +0200 Subject: [PATCH] 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 --- .../RecordBoardBodyEscapeHotkeyEffect.tsx | 42 +++--- .../components/RecordBoardHotkeyEffect.tsx | 135 ++---------------- .../constants/RecordBoardFocusId.ts | 1 + .../hooks/useFocusedRecordBoardCard.ts | 66 ++++++++- .../hooks/useRecordBoardArrowKeysEffect.ts | 46 ++++++ .../hooks/useRecordBoardCardHotkeys.ts | 91 ++++++++++++ .../hooks/useRecordBoardSelectAllHotkeys.ts | 47 ++++++ .../RecordBoardCardDraggableContainer.tsx | 6 +- .../RecordBoardCardFocusHotkeyEffect.tsx | 57 -------- .../RecordBoardCardHotkeysEffect.tsx | 22 +++ .../utils/getRecordBoardCardFocusId.ts | 11 ++ .../hooks/useOpenFieldInputEditMode.ts | 40 ++++-- .../components/MultiSelectFieldInput.tsx | 6 +- .../components/RelationFromManyFieldInput.tsx | 4 +- .../components/RelationToOneFieldInput.tsx | 4 +- .../input/components/SelectFieldInput.tsx | 6 +- .../RelationFromManyFieldInput.stories.tsx | 4 +- .../RelationToOneFieldInput.stories.tsx | 8 +- .../useOpenRelationFromManyFieldInput.tsx | 4 +- .../hooks/useOpenRelationToOneFieldInput.tsx | 6 +- ...getRelationFromManyFieldInputInstanceId.ts | 9 -- .../getRelationToOneFieldInputInstanceId.ts | 9 -- .../utils/getFieldInputInstanceId.ts | 11 +- .../components/RecordInlineCell.tsx | 52 +++++-- ...ineCellCloseOnCommandMenuOpeningEffect.tsx | 14 ++ .../components/RecordInlineCellContainer.tsx | 4 + .../record-inline-cell/hooks/useInlineCell.ts | 6 - .../useCloseCurrentTableCellInEditMode.ts | 2 +- .../focus/types/FocusComponentType.ts | 4 +- 29 files changed, 436 insertions(+), 281 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFocusId.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardArrowKeysEffect.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx index 717aaf11f..8ea830af3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx @@ -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; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx index d60c71cb7..8f7cb1226 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHotkeyEffect.tsx @@ -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; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFocusId.ts b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFocusId.ts new file mode 100644 index 000000000..3b5f6c4d2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFocusId.ts @@ -0,0 +1 @@ +export const RECORD_BOARD_FOCUS_ID = 'record-board'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts index 059d33537..5a0fff050 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useFocusedRecordBoardCard.ts @@ -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 { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardArrowKeysEffect.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardArrowKeysEffect.ts new file mode 100644 index 000000000..4ab03437d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardArrowKeysEffect.ts @@ -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], + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts new file mode 100644 index 000000000..23b95d499 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts @@ -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], + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys.ts new file mode 100644 index 000000000..d4b926d7d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys.ts @@ -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, + }, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx index 0a43c3587..460f30997 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer.tsx @@ -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 && ( - - )} + {isRecordBoardCardFocusActive && } )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx deleted file mode 100644 index fffa7eb11..000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardFocusHotkeyEffect.tsx +++ /dev/null @@ -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; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect.tsx new file mode 100644 index 000000000..6189280e9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId.ts new file mode 100644 index 000000000..9044a9e84 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/utils/getRecordBoardCardFocusId.ts @@ -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}`; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts index bdd6a10d6..422e66f63 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts @@ -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; + recordId: string; + }) => { + removeFocusItemFromFocusStackById({ + focusId: getFieldInputInstanceId({ + recordId, + fieldName: fieldDefinition.metadata.fieldName, + }), + }); + }; + return { - openFieldInput: openFieldInput, - closeFieldInput: () => {}, + openFieldInput, + closeFieldInput, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index d79830c99..a57257559 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -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} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index d36382078..af209a1c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -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, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx index b8e723e10..f32632f87 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx @@ -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, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index 83b467d88..ce6454c76 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx index 701bb4039..ac0702157 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx @@ -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 = () => {
{ 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', }), diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx index 1f1d8128c..161572174 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx @@ -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, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx index 170d72524..9a84935ad 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx @@ -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 diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId.ts deleted file mode 100644 index 1f90109e1..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const getRelationFromManyFieldInputInstanceId = ({ - recordId, - fieldName, -}: { - recordId: string; - fieldName: string; -}): string => { - return `relation-from-many-field-input-${recordId}-${fieldName}`; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId.ts deleted file mode 100644 index 2e629f625..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const getRelationToOneFieldInputInstanceId = ({ - recordId, - fieldName, -}: { - recordId: string; - fieldName: string; -}): string => { - return `relation-to-one-field-input-${recordId}-${fieldName}`; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldInputInstanceId.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldInputInstanceId.ts index 3a5f0bba9..70c4a1a22 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldInputInstanceId.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldInputInstanceId.ts @@ -1,6 +1,9 @@ -export const getFieldInputInstanceId = ( - recordId: string, - fieldName: string, -) => { +export const getFieldInputInstanceId = ({ + recordId, + fieldName, +}: { + recordId: string; + fieldName: string; +}) => { return `${recordId}-${fieldName}`; }; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 8172584b4..207212802 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -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 ( diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect.tsx new file mode 100644 index 000000000..c23a83054 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index fa81021da..59115c498 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -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 = () => { )} )} + {isInlineCellInEditMode && ( + + )} diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts index 84756f7e2..6ced59938 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts @@ -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(); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts index 175923cea..4540b7fc7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts @@ -27,7 +27,7 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { goBackToPreviousDropdownFocusId(); removeLastFocusItemFromFocusStackByComponentType({ - componentType: FocusComponentType.OPEN_FIELD_INPUT, + componentType: FocusComponentType.OPENED_FIELD_INPUT, }); }; }, diff --git a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts index 65518edfc..0fc4b1618 100644 --- a/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts +++ b/packages/twenty-front/src/modules/ui/utilities/focus/types/FocusComponentType.ts @@ -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', }