Fix a hotkey scope race condition in command menu (#13025)

Fixes https://github.com/twentyhq/twenty/issues/12885

This PR fixes a hotkey scope race condition happening on note/task
creation.

The problem is that `ActivityRichTextEditor` catches the click event
before the title cell.

So here we prevent this from happening by checking if the record title
cell is.

This is only temporary and should be improved after the persist logic
refactor : https://github.com/twentyhq/core-team-issues/issues/192
This commit is contained in:
Lucas Bordeau
2025-07-03 14:04:37 +02:00
committed by GitHub
parent 74c8ab422b
commit 1f1318febf
5 changed files with 147 additions and 53 deletions

View File

@ -8,8 +8,6 @@ import {
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { useInlineCell } from '../../record-inline-cell/hooks/useInlineCell';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
import {
@ -18,6 +16,7 @@ import {
} from '@/object-record/record-title-cell/components/RecordTitleCellContext';
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
@ -36,35 +35,50 @@ export const RecordTitleCell = ({
const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell(
getRecordTitleCellId(
recordId,
fieldDefinition?.fieldMetadataId,
containerType,
),
);
const { closeRecordTitleCell } = useRecordTitleCell();
const handleEnter: FieldInputEvent = (persistField) => {
closeInlineCell();
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
};
const handleEscape = () => {
closeInlineCell();
const handleEscape: FieldInputEvent = (persistField) => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
};
const handleTab: FieldInputEvent = (persistField) => {
closeInlineCell();
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
};
const handleShiftTab: FieldInputEvent = (persistField) => {
closeInlineCell();
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
};
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
closeInlineCell();
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
};

View File

@ -1,9 +1,7 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { Theme, withTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
@ -40,18 +38,16 @@ export const RecordTitleCellSingleTextDisplayMode = () => {
const isEmpty =
recordValue?.[fieldDefinition.metadata.fieldName]?.trim() === '';
const { openInlineCell } = useInlineCell();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { openRecordTitleCell } = useRecordTitleCell();
return (
<StyledDiv
onClick={() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: TitleInputHotkeyScope.TitleInput,
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
openRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition.fieldMetadataId,
containerType: RecordTitleCellContainerType.ShowPage,
});
openInlineCell();
}}
>
{isEmpty ? (

View File

@ -6,7 +6,9 @@ import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -15,10 +17,9 @@ export const useRecordTitleCell = () => {
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const closeRecordTitleCell = useRecoilCallback(
({ set }) =>
@ -38,11 +39,17 @@ export const useRecordTitleCell = () => {
false,
);
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
removeFocusItemFromFocusStackById({
focusId: getRecordTitleCellId(
recordId,
fieldMetadataId,
containerType,
),
});
goBackToPreviousDropdownFocusId();
},
[goBackToPreviousDropdownFocusId, goBackToPreviousHotkeyScope],
[goBackToPreviousDropdownFocusId, removeFocusItemFromFocusStackById],
);
const initFieldInputDraftValue = useInitDraftValueV2();
@ -61,14 +68,41 @@ export const useRecordTitleCell = () => {
customEditHotkeyScopeForField?: HotkeyScope;
}) => {
if (isDefined(customEditHotkeyScopeForField)) {
setHotkeyScopeAndMemorizePreviousScope({
scope: customEditHotkeyScopeForField.scope,
customScopes: customEditHotkeyScopeForField.customScopes,
pushFocusItemToFocusStack({
focusId: getRecordTitleCellId(
recordId,
fieldMetadataId,
containerType,
),
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId(
recordId,
fieldMetadataId,
containerType,
),
},
hotkeyScope: customEditHotkeyScopeForField,
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
});
} else {
setHotkeyScopeAndMemorizePreviousScope({
scope: TitleInputHotkeyScope.TitleInput,
pushFocusItemToFocusStack({
focusId: getRecordTitleCellId(
recordId,
fieldMetadataId,
containerType,
),
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId(
recordId,
fieldMetadataId,
containerType,
),
},
hotkeyScope: {
scope: TitleInputHotkeyScope.TitleInput,
},
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
});
}
@ -98,7 +132,7 @@ export const useRecordTitleCell = () => {
fieldComponentInstanceId: recordTitleCellId,
});
},
[initFieldInputDraftValue, setHotkeyScopeAndMemorizePreviousScope],
[initFieldInputDraftValue, pushFocusItemToFocusStack],
);
return {