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:
@ -11,7 +11,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||||
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
@ -33,7 +32,14 @@ import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords
|
|||||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||||
|
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
|
||||||
|
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||||
|
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||||
|
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
|
||||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||||
|
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 type { PartialBlock } from '@blocknote/core';
|
import type { PartialBlock } from '@blocknote/core';
|
||||||
import '@blocknote/core/fonts/inter.css';
|
import '@blocknote/core/fonts/inter.css';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
@ -86,6 +92,10 @@ export const ActivityRichTextEditor = ({
|
|||||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||||
|
const { removeFocusItemFromFocusStackById } =
|
||||||
|
useRemoveFocusItemFromFocusStackById();
|
||||||
|
|
||||||
const { fetchAllRecords: findSoftDeletedAttachments } =
|
const { fetchAllRecords: findSoftDeletedAttachments } =
|
||||||
useLazyFetchAllRecords({
|
useLazyFetchAllRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||||
@ -96,11 +106,6 @@ export const ActivityRichTextEditor = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
goBackToPreviousHotkeyScope,
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
|
||||||
} = usePreviousHotkeyScope();
|
|
||||||
|
|
||||||
const { upsertActivity } = useUpsertActivity({
|
const { upsertActivity } = useUpsertActivity({
|
||||||
activityObjectNameSingular: activityObjectNameSingular,
|
activityObjectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
@ -354,16 +359,60 @@ export const ActivityRichTextEditor = ({
|
|||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({
|
||||||
|
objectNameSingular: activityObjectNameSingular,
|
||||||
|
objectRecordId: activityId,
|
||||||
|
});
|
||||||
|
|
||||||
const handleBlockEditorFocus = () => {
|
const recordTitleCellId = getRecordTitleCellId(
|
||||||
setHotkeyScopeAndMemorizePreviousScope({
|
activityId,
|
||||||
scope: ActivityEditorHotkeyScope.ActivityBody,
|
labelIdentifierFieldMetadataItem?.id,
|
||||||
});
|
RecordTitleCellContainerType.ShowPage,
|
||||||
};
|
);
|
||||||
|
|
||||||
const handlerBlockEditorBlur = () => {
|
const handleBlockEditorFocus = useRecoilCallback(
|
||||||
goBackToPreviousHotkeyScope();
|
({ snapshot }) =>
|
||||||
};
|
() => {
|
||||||
|
const isRecordTitleCellOpen = snapshot
|
||||||
|
.getLoadable(isInlineCellInEditModeScopedState(recordTitleCellId))
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (isRecordTitleCellOpen) {
|
||||||
|
editor.domElement?.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushFocusItemToFocusStack({
|
||||||
|
component: {
|
||||||
|
instanceId: activityId,
|
||||||
|
type: FocusComponentType.ACTIVITY_RICH_TEXT_EDITOR,
|
||||||
|
},
|
||||||
|
focusId: activityId,
|
||||||
|
hotkeyScope: {
|
||||||
|
scope: ActivityEditorHotkeyScope.ActivityBody,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[recordTitleCellId, activityId, editor, pushFocusItemToFocusStack],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlerBlockEditorBlur = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const isRecordTitleCellOpen = snapshot
|
||||||
|
.getLoadable(isInlineCellInEditModeScopedState(recordTitleCellId))
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (isRecordTitleCellOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFocusItemFromFocusStackById({
|
||||||
|
focusId: activityId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[activityId, recordTitleCellId, removeFocusItemFromFocusStackById],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import {
|
|||||||
FieldInputEvent,
|
FieldInputEvent,
|
||||||
} from '@/object-record/record-field/types/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 { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||||
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
|
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
|
||||||
import {
|
import {
|
||||||
@ -18,6 +16,7 @@ import {
|
|||||||
} from '@/object-record/record-title-cell/components/RecordTitleCellContext';
|
} from '@/object-record/record-title-cell/components/RecordTitleCellContext';
|
||||||
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
|
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
|
||||||
import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
|
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 { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||||
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
|
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
|
||||||
|
|
||||||
@ -36,35 +35,50 @@ export const RecordTitleCell = ({
|
|||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
const isFieldInputOnly = useIsFieldInputOnly();
|
||||||
|
|
||||||
const { closeInlineCell } = useInlineCell(
|
const { closeRecordTitleCell } = useRecordTitleCell();
|
||||||
getRecordTitleCellId(
|
|
||||||
recordId,
|
|
||||||
fieldDefinition?.fieldMetadataId,
|
|
||||||
containerType,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEnter: FieldInputEvent = (persistField) => {
|
const handleEnter: FieldInputEvent = (persistField) => {
|
||||||
closeInlineCell();
|
closeRecordTitleCell({
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId: fieldDefinition?.fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
});
|
||||||
persistField();
|
persistField();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape: FieldInputEvent = (persistField) => {
|
||||||
closeInlineCell();
|
closeRecordTitleCell({
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId: fieldDefinition?.fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
});
|
||||||
|
persistField();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab: FieldInputEvent = (persistField) => {
|
const handleTab: FieldInputEvent = (persistField) => {
|
||||||
closeInlineCell();
|
closeRecordTitleCell({
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId: fieldDefinition?.fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
});
|
||||||
persistField();
|
persistField();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||||
closeInlineCell();
|
closeRecordTitleCell({
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId: fieldDefinition?.fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
});
|
||||||
persistField();
|
persistField();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
|
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
|
||||||
closeInlineCell();
|
closeRecordTitleCell({
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId: fieldDefinition?.fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
});
|
||||||
persistField();
|
persistField();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
|
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||||
import { Theme, withTheme } from '@emotion/react';
|
import { Theme, withTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
@ -40,18 +38,16 @@ export const RecordTitleCellSingleTextDisplayMode = () => {
|
|||||||
const isEmpty =
|
const isEmpty =
|
||||||
recordValue?.[fieldDefinition.metadata.fieldName]?.trim() === '';
|
recordValue?.[fieldDefinition.metadata.fieldName]?.trim() === '';
|
||||||
|
|
||||||
const { openInlineCell } = useInlineCell();
|
const { openRecordTitleCell } = useRecordTitleCell();
|
||||||
|
|
||||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv
|
<StyledDiv
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHotkeyScopeAndMemorizePreviousScope({
|
openRecordTitleCell({
|
||||||
scope: TitleInputHotkeyScope.TitleInput,
|
recordId,
|
||||||
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||||
|
containerType: RecordTitleCellContainerType.ShowPage,
|
||||||
});
|
});
|
||||||
openInlineCell();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEmpty ? (
|
{isEmpty ? (
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/
|
|||||||
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
|
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
|
||||||
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
|
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
|
||||||
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
|
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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
@ -15,10 +17,9 @@ export const useRecordTitleCell = () => {
|
|||||||
const { goBackToPreviousDropdownFocusId } =
|
const { goBackToPreviousDropdownFocusId } =
|
||||||
useGoBackToPreviousDropdownFocusId();
|
useGoBackToPreviousDropdownFocusId();
|
||||||
|
|
||||||
const {
|
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
const { removeFocusItemFromFocusStackById } =
|
||||||
goBackToPreviousHotkeyScope,
|
useRemoveFocusItemFromFocusStackById();
|
||||||
} = usePreviousHotkeyScope();
|
|
||||||
|
|
||||||
const closeRecordTitleCell = useRecoilCallback(
|
const closeRecordTitleCell = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
@ -38,11 +39,17 @@ export const useRecordTitleCell = () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope(INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY);
|
removeFocusItemFromFocusStackById({
|
||||||
|
focusId: getRecordTitleCellId(
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
goBackToPreviousDropdownFocusId();
|
goBackToPreviousDropdownFocusId();
|
||||||
},
|
},
|
||||||
[goBackToPreviousDropdownFocusId, goBackToPreviousHotkeyScope],
|
[goBackToPreviousDropdownFocusId, removeFocusItemFromFocusStackById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const initFieldInputDraftValue = useInitDraftValueV2();
|
const initFieldInputDraftValue = useInitDraftValueV2();
|
||||||
@ -61,14 +68,41 @@ export const useRecordTitleCell = () => {
|
|||||||
customEditHotkeyScopeForField?: HotkeyScope;
|
customEditHotkeyScopeForField?: HotkeyScope;
|
||||||
}) => {
|
}) => {
|
||||||
if (isDefined(customEditHotkeyScopeForField)) {
|
if (isDefined(customEditHotkeyScopeForField)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope({
|
pushFocusItemToFocusStack({
|
||||||
scope: customEditHotkeyScopeForField.scope,
|
focusId: getRecordTitleCellId(
|
||||||
customScopes: customEditHotkeyScopeForField.customScopes,
|
recordId,
|
||||||
|
fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
),
|
||||||
|
component: {
|
||||||
|
type: FocusComponentType.OPENED_FIELD_INPUT,
|
||||||
|
instanceId: getRecordTitleCellId(
|
||||||
|
recordId,
|
||||||
|
fieldMetadataId,
|
||||||
|
containerType,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
hotkeyScope: customEditHotkeyScopeForField,
|
||||||
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setHotkeyScopeAndMemorizePreviousScope({
|
pushFocusItemToFocusStack({
|
||||||
scope: TitleInputHotkeyScope.TitleInput,
|
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,
|
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,7 +132,7 @@ export const useRecordTitleCell = () => {
|
|||||||
fieldComponentInstanceId: recordTitleCellId,
|
fieldComponentInstanceId: recordTitleCellId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[initFieldInputDraftValue, setHotkeyScopeAndMemorizePreviousScope],
|
[initFieldInputDraftValue, pushFocusItemToFocusStack],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -9,4 +9,5 @@ export enum FocusComponentType {
|
|||||||
RECORD_TABLE_CELL = 'record-table-cell',
|
RECORD_TABLE_CELL = 'record-table-cell',
|
||||||
RECORD_BOARD = 'record-board',
|
RECORD_BOARD = 'record-board',
|
||||||
RECORD_BOARD_CARD = 'record-board-card',
|
RECORD_BOARD_CARD = 'record-board-card',
|
||||||
|
ACTIVITY_RICH_TEXT_EDITOR = 'activity-rich-text-editor',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user