This PR fixes many small bugs around the recent hotkey scope refactor.

- Removed unused ActionBar files
- Created components CommandMenuOpenContainer and
KeyboardShortcutMenuOpenContent to avoid mounting listeners when not
needed
- Added DEFAULT_CELL_SCOPE where missing in some field inputs
- Called setHotkeyScopeAndMemorizePreviousScope instead of
setHotkeyScope in new useOpenFieldInputEditMode hook
- Broke down RecordTableBodyUnselectEffect into multiple simpler effect
components that are mounted only when needed to avoid listening for
keyboard and clickoutside event
- Re-implemented recently deleted table cell soft focus component logic
into RecordTableCellDisplayMode
- Created component selector isAtLeastOneTableRowSelectedSelector
- Drill down hotkey scope when opening a dropdown
- Improved debug logs
This commit is contained in:
Lucas Bordeau
2025-04-09 18:34:31 +02:00
committed by GitHub
parent 9f4e8c046f
commit 2b77f598b2
25 changed files with 362 additions and 194 deletions

View File

@ -1,9 +0,0 @@
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
describe('getActionBarIdFromActionMenuId', () => {
it('should return the correct action bar id', () => {
expect(getActionBarIdFromActionMenuId('action-menu-id')).toBe(
'action-bar-action-menu-id',
);
});
});

View File

@ -1,3 +0,0 @@
export const getActionBarIdFromActionMenuId = (actionMenuId: string) => {
return `action-bar-${actionMenuId}`;
};

View File

@ -1,12 +1,8 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants'; import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
@ -15,78 +11,20 @@ import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/reco
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTheme } from '@emotion/react'; import { AnimatePresence } from 'framer-motion';
import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledCommandMenu = styled(motion.div)`
background: ${({ theme }) => theme.background.primary};
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
font-family: ${({ theme }) => theme.font.family};
height: 100%;
overflow: hidden;
padding: 0;
position: fixed;
right: 0%;
top: 0%;
z-index: ${RootStackingContextZIndices.CommandMenu};
display: flex;
flex-direction: column;
`;
export const CommandMenuContainer = ({ export const CommandMenuContainer = ({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const { closeCommandMenu } = useCommandMenu();
const { commandMenuCloseAnimationCompleteCleanup } = const { commandMenuCloseAnimationCompleteCleanup } =
useCommandMenuCloseAnimationCompleteCleanup(); useCommandMenuCloseAnimationCompleteCleanup();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const commandMenuRef = useRef<HTMLDivElement>(null);
useCommandMenuHotKeys();
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
() => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope?.scope === CommandMenuHotkeyScope.CommandMenuFocused) {
closeCommandMenu();
}
},
[closeCommandMenu],
);
useListenClickOutside({
refs: [commandMenuRef],
callback: handleClickOutside,
listenerId: 'COMMAND_MENU_LISTENER_ID',
excludeClassNames: ['page-header-command-menu-button'],
});
const isMobile = useIsMobile();
const targetVariantForAnimation: CommandMenuAnimationVariant = isMobile
? 'fullScreen'
: 'normal';
const theme = useTheme();
const objectMetadataItemId = useRecoilComponentValueV2( const objectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState, contextStoreCurrentObjectMetadataItemIdComponentState,
COMMAND_MENU_COMPONENT_INSTANCE_ID, COMMAND_MENU_COMPONENT_INSTANCE_ID,
@ -129,18 +67,9 @@ export const CommandMenuContainer = ({
onExitComplete={commandMenuCloseAnimationCompleteCleanup} onExitComplete={commandMenuCloseAnimationCompleteCleanup}
> >
{isCommandMenuOpened && ( {isCommandMenuOpened && (
<StyledCommandMenu <CommandMenuOpenContainer>
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children} {children}
</StyledCommandMenu> </CommandMenuOpenContainer>
)} )}
</AnimatePresence> </AnimatePresence>
</ActionMenuComponentInstanceContext.Provider> </ActionMenuComponentInstanceContext.Provider>

View File

@ -0,0 +1,85 @@
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilCallback } from 'recoil';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledCommandMenu = styled(motion.div)`
background: ${({ theme }) => theme.background.primary};
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
font-family: ${({ theme }) => theme.font.family};
height: 100%;
overflow: hidden;
padding: 0;
position: fixed;
right: 0%;
top: 0%;
z-index: ${RootStackingContextZIndices.CommandMenu};
display: flex;
flex-direction: column;
`;
export const CommandMenuOpenContainer = ({
children,
}: React.PropsWithChildren) => {
const isMobile = useIsMobile();
const targetVariantForAnimation: CommandMenuAnimationVariant = isMobile
? 'fullScreen'
: 'normal';
const theme = useTheme();
const { closeCommandMenu } = useCommandMenu();
const commandMenuRef = useRef<HTMLDivElement>(null);
useCommandMenuHotKeys();
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
() => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope?.scope === CommandMenuHotkeyScope.CommandMenuFocused) {
closeCommandMenu();
}
},
[closeCommandMenu],
);
useListenClickOutside({
refs: [commandMenuRef],
callback: handleClickOutside,
listenerId: 'COMMAND_MENU_LISTENER_ID',
excludeClassNames: ['page-header-command-menu-button'],
});
return (
<StyledCommandMenu
data-testid="command-menu"
ref={commandMenuRef}
className="command-menu"
animate={targetVariantForAnimation}
initial="closed"
exit="closed"
variants={COMMAND_MENU_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
>
{children}
</StyledCommandMenu>
);
};

View File

@ -1,22 +1,16 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral';
import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu'; import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState'; import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog'; import { KeyboardShortcutMenuOpenContent } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent';
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
export const KeyboardShortcutMenu = () => { export const KeyboardShortcutMenu = () => {
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } = const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
useKeyboardShortcutMenu();
const isKeyboardShortcutMenuOpened = useRecoilValue( const isKeyboardShortcutMenuOpened = useRecoilValue(
isKeyboardShortcutMenuOpenedState, isKeyboardShortcutMenuOpenedState,
); );
@ -32,31 +26,7 @@ export const KeyboardShortcutMenu = () => {
[toggleKeyboardShortcutMenu], [toggleKeyboardShortcutMenu],
); );
useScopedHotkeys(
[Key.Escape],
() => {
closeKeyboardShortcutMenu();
},
AppHotkeyScope.KeyboardShortcutMenuOpen,
[closeKeyboardShortcutMenu],
);
return ( return (
<> <>{isKeyboardShortcutMenuOpened && <KeyboardShortcutMenuOpenContent />}</>
{isKeyboardShortcutMenuOpened && (
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
<KeyboardMenuGroup heading="Table">
{KEYBOARD_SHORTCUTS_TABLE.map((TableShortcut, index) => (
<KeyboardMenuItem shortcut={TableShortcut} key={index} />
))}
</KeyboardMenuGroup>
<KeyboardMenuGroup heading="General">
{KEYBOARD_SHORTCUTS_GENERAL.map((GeneralShortcut) => (
<KeyboardMenuItem shortcut={GeneralShortcut} />
))}
</KeyboardMenuGroup>
</KeyboardMenuDialog>
)}
</>
); );
}; };

View File

@ -0,0 +1,43 @@
import { Key } from 'ts-key-enum';
import { KEYBOARD_SHORTCUTS_GENERAL } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral';
import { KEYBOARD_SHORTCUTS_TABLE } from '@/keyboard-shortcut-menu/constants/KeyboardShortcutsTable';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
export const KeyboardShortcutMenuOpenContent = () => {
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
useKeyboardShortcutMenu();
useScopedHotkeys(
[Key.Escape],
() => {
closeKeyboardShortcutMenu();
},
AppHotkeyScope.KeyboardShortcutMenuOpen,
[closeKeyboardShortcutMenu],
);
return (
<>
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
<KeyboardMenuGroup heading="Table">
{KEYBOARD_SHORTCUTS_TABLE.map((TableShortcut, index) => (
<KeyboardMenuItem shortcut={TableShortcut} key={index} />
))}
</KeyboardMenuGroup>
<KeyboardMenuGroup heading="General">
{KEYBOARD_SHORTCUTS_GENERAL.map((GeneralShortcut) => (
<KeyboardMenuItem shortcut={GeneralShortcut} />
))}
</KeyboardMenuGroup>
</KeyboardMenuDialog>
</>
);
};

View File

@ -2,7 +2,6 @@ import styled from '@emotion/styled';
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useContext, useRef } from 'react'; import { useContext, useRef } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
@ -136,8 +135,6 @@ export const RecordBoard = () => {
useScopedHotkeys('ctrl+a,meta+a', selectAll, TableHotkeyScope.Table); useScopedHotkeys('ctrl+a,meta+a', selectAll, TableHotkeyScope.Table);
useScopedHotkeys(Key.Escape, resetRecordSelection, TableHotkeyScope.Table);
const setIsRemoveSortingModalOpen = useSetRecoilState( const setIsRemoveSortingModalOpen = useSetRecoilState(
isRemoveSortingModalOpenState, isRemoveSortingModalOpenState,
); );

View File

@ -18,7 +18,7 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -30,7 +30,7 @@ export const useOpenFieldInputEditMode = () => {
const { openActivityTargetCellEditMode } = const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode(); useOpenActivityTargetCellEditMode();
const setHotkeyScope = useSetHotkeyScope(); const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const openFieldInput = useRecoilCallback( const openFieldInput = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
@ -102,7 +102,7 @@ export const useOpenFieldInputEditMode = () => {
} }
} }
setHotkeyScope( setHotkeyScopeAndMemorizePreviousScope(
DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes, DEFAULT_CELL_SCOPE.customScopes,
); );
@ -111,7 +111,7 @@ export const useOpenFieldInputEditMode = () => {
openActivityTargetCellEditMode, openActivityTargetCellEditMode,
openRelationFromManyFieldInput, openRelationFromManyFieldInput,
openRelationToOneFieldInput, openRelationToOneFieldInput,
setHotkeyScope, setHotkeyScopeAndMemorizePreviousScope,
], ],
); );

View File

@ -9,6 +9,7 @@ import {
FieldInputClickOutsideEvent, FieldInputClickOutsideEvent,
FieldInputEvent, FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent'; } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
type FullNameFieldInputProps = { type FullNameFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent; onClickOutside?: FieldInputClickOutsideEvent;
@ -25,7 +26,7 @@ export const FullNameFieldInput = ({
onTab, onTab,
onShiftTab, onShiftTab,
}: FullNameFieldInputProps) => { }: FullNameFieldInputProps) => {
const { draftValue, setDraftValue, persistFullNameField, fieldDefinition } = const { draftValue, setDraftValue, persistFullNameField } =
useFullNameField(); useFullNameField();
const convertToFullName = (newDoubleText: FieldDoubleText) => { const convertToFullName = (newDoubleText: FieldDoubleText) => {
@ -93,7 +94,7 @@ export const FullNameFieldInput = ({
onShiftTab={handleShiftTab} onShiftTab={handleShiftTab}
onTab={handleTab} onTab={handleTab}
onPaste={handlePaste} onPaste={handlePaste}
hotkeyScope={`full-name-field-input-${fieldDefinition.metadata.fieldName}`} hotkeyScope={DEFAULT_CELL_SCOPE.scope}
onChange={handleChange} onChange={handleChange}
/> />
); );

View File

@ -50,7 +50,7 @@ export const SelectFieldInput = ({
onCancel?.(); onCancel?.();
resetSelectedItem(); resetSelectedItem();
}, },
`select-field-input-${fieldDefinition.metadata.fieldName}`, DEFAULT_CELL_SCOPE.scope,
[onCancel, resetSelectedItem], [onCancel, resetSelectedItem],
); );

View File

@ -14,7 +14,7 @@ import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFie
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useIcons } from 'twenty-ui/display'; import { useIcons } from 'twenty-ui/display';
@ -80,9 +80,11 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
const hotkeyScope = snapshot const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState) .getLoadable(currentHotkeyScopeState)
.getValue(); .getValue();
if (hotkeyScope.scope !== InlineCellHotkeyScope.InlineCell) {
if (hotkeyScope.scope !== DEFAULT_CELL_SCOPE.scope) {
return; return;
} }
event.stopImmediatePropagation(); event.stopImmediatePropagation();
persistField(); persistField();

View File

@ -1,6 +1,11 @@
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect'; import { RecordTableBodyEscapeHotkeyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEscapeHotkeyEffect';
import { RecordTableBodySoftFocusClickOutsideEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodySoftFocusClickOutsideEffect';
import { RecordTableBodySoftFocusKeyboardEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodySoftFocusKeyboardEffect';
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect'; import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects'; import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
import { isAtLeastOneTableRowSelectedSelector } from '@/object-record/record-table/record-table-row/states/isAtLeastOneTableRowSelectedSelector';
import { isSoftFocusActiveComponentState } from '@/object-record/record-table/states/isSoftFocusActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export interface RecordTableBodyEffectsWrapperProps { export interface RecordTableBodyEffectsWrapperProps {
hasRecordGroups: boolean; hasRecordGroups: boolean;
@ -10,13 +15,29 @@ export interface RecordTableBodyEffectsWrapperProps {
export const RecordTableBodyEffectsWrapper = ({ export const RecordTableBodyEffectsWrapper = ({
hasRecordGroups, hasRecordGroups,
tableBodyRef, tableBodyRef,
}: RecordTableBodyEffectsWrapperProps) => ( }: RecordTableBodyEffectsWrapperProps) => {
<> const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
{hasRecordGroups ? ( isAtLeastOneTableRowSelectedSelector,
<RecordTableRecordGroupBodyEffects /> );
) : (
<RecordTableNoRecordGroupBodyEffect /> const isSoftFocusActiveState = useRecoilComponentValueV2(
)} isSoftFocusActiveComponentState,
<RecordTableBodyUnselectEffect tableBodyRef={tableBodyRef} /> );
</>
); return (
<>
{hasRecordGroups ? (
<RecordTableRecordGroupBodyEffects />
) : (
<RecordTableNoRecordGroupBodyEffect />
)}
{isAtLeastOneRecordSelected && <RecordTableBodyEscapeHotkeyEffect />}
{isSoftFocusActiveState && <RecordTableBodySoftFocusKeyboardEffect />}
{isSoftFocusActiveState && (
<RecordTableBodySoftFocusClickOutsideEffect
tableBodyRef={tableBodyRef}
/>
)}
</>
);
};

View File

@ -10,14 +10,12 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField'; import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum'; import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
const StyledTableContainer = styled.div` const StyledTableContainer = styled.div`
display: flex; display: flex;
@ -39,10 +37,9 @@ export const RecordTableWithWrappers = ({
recordTableId, recordTableId,
viewBarId, viewBarId,
}: RecordTableWithWrappersProps) => { }: RecordTableWithWrappersProps) => {
const { resetTableRowSelection, selectAllRows, setHasUserSelectedAllRows } = const { selectAllRows, setHasUserSelectedAllRows } = useRecordTable({
useRecordTable({ recordTableId,
recordTableId, });
});
const handleSelectAllRows = () => { const handleSelectAllRows = () => {
setHasUserSelectedAllRows(true); setHasUserSelectedAllRows(true);
@ -55,8 +52,6 @@ export const RecordTableWithWrappers = ({
TableHotkeyScope.Table, TableHotkeyScope.Table,
); );
useScopedHotkeys(Key.Escape, resetTableRowSelection, TableHotkeyScope.Table);
const { saveViewFields } = useSaveCurrentViewFields(); const { saveViewFields } = useSaveCurrentViewFields();
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular }); const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });

View File

@ -0,0 +1,24 @@
import { Key } from 'ts-key-enum';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
export const RecordTableBodyEscapeHotkeyEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const { resetTableRowSelection } = useRecordTable({
recordTableId,
});
useScopedHotkeys(
[Key.Escape],
() => {
resetTableRowSelection();
},
TableHotkeyScope.Table,
);
return <></>;
};

View File

@ -1,38 +1,19 @@
import { Key } from 'ts-key-enum';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus'; import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
type RecordTableBodyUnselectEffectProps = { type RecordTableBodySoftFocusClickOutsideEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>; tableBodyRef: React.RefObject<HTMLDivElement>;
}; };
export const RecordTableBodyUnselectEffect = ({ export const RecordTableBodySoftFocusClickOutsideEffect = ({
tableBodyRef, tableBodyRef,
}: RecordTableBodyUnselectEffectProps) => { }: RecordTableBodySoftFocusClickOutsideEffectProps) => {
const { recordTableId } = useRecordTableContextOrThrow(); const { recordTableId } = useRecordTableContextOrThrow();
const leaveTableFocus = useLeaveTableFocus(recordTableId); const leaveTableFocus = useLeaveTableFocus(recordTableId);
const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({
recordTableId,
});
useMapKeyboardToSoftFocus();
useScopedHotkeys(
[Key.Escape],
() => {
resetTableRowSelection();
},
TableHotkeyScope.Table,
);
useListenClickOutside({ useListenClickOutside({
excludeClassNames: [ excludeClassNames: [
'bottom-bar', 'bottom-bar',

View File

@ -0,0 +1,14 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
export const RecordTableBodySoftFocusKeyboardEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const { useMapKeyboardToSoftFocus } = useRecordTable({
recordTableId,
});
useMapKeyboardToSoftFocus();
return <></>;
};

View File

@ -1,26 +1,55 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useContext } from 'react'; import { useContext } from 'react';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer'; import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
export const RecordTableCellDisplayMode = ({ export const RecordTableCellDisplayMode = ({
children, children,
softFocus, }: React.PropsWithChildren) => {
}: React.PropsWithChildren<{ softFocus?: boolean }>) => { const { recordId, isReadOnly } = useContext(FieldContext);
const { recordId } = useContext(FieldContext);
const { columnIndex, hasSoftFocus } = useContext(RecordTableCellContext);
const isMobile = useIsMobile();
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow(); const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
const { openTableCell } = useOpenRecordTableCellFromCell();
const isFieldInputOnly = useIsFieldInputOnly();
const isFirstColumn = columnIndex === 0;
const showButton =
hasSoftFocus &&
!isFieldInputOnly &&
!isReadOnly &&
!(isMobile && isFirstColumn);
const handleActionMenuDropdown = (event: React.MouseEvent) => { const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId); onActionMenuDropdownOpened(event, recordId);
}; };
const handleClick = () => {
if (!isFieldInputOnly && !isReadOnly) {
openTableCell();
}
};
return ( return (
<RecordTableCellDisplayContainer <>
softFocus={softFocus} <RecordTableCellDisplayContainer
onContextMenu={handleActionMenuDropdown} softFocus={hasSoftFocus}
> onContextMenu={handleActionMenuDropdown}
{children} onClick={handleClick}
</RecordTableCellDisplayContainer> >
{children}
</RecordTableCellDisplayContainer>
{showButton && <RecordTableCellEditButton />}
</>
); );
}; };

View File

@ -0,0 +1,36 @@
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight, IconPencil } from 'twenty-ui/display';
export const RecordTableCellEditButton = () => {
const { columnIndex } = useContext(RecordTableCellContext);
const { openTableCell } = useOpenRecordTableCellFromCell();
const isFieldInputOnly = useIsFieldInputOnly();
const isFirstColumn = columnIndex === 0;
const customButtonIcon = useGetButtonIcon();
const buttonIcon = isFirstColumn
? IconArrowUpRight
: isDefined(customButtonIcon)
? customButtonIcon
: IconPencil;
const handleButtonClick = () => {
if (!isFieldInputOnly && isFirstColumn) {
openTableCell(undefined, false, true);
} else {
openTableCell();
}
};
return (
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
);
};

View File

@ -0,0 +1,30 @@
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
export const isAtLeastOneTableRowSelectedSelector =
createComponentSelectorV2<boolean>({
key: 'isAtLeastOneTableRowSelectedSelector',
componentInstanceContext: RecordTableComponentInstanceContext,
get:
({ instanceId }) =>
({ get }) => {
const allRecordIds = get(
recordIndexAllRecordIdsComponentSelector.selectorFamily({
instanceId,
}),
);
const isAnyRecordSelected = allRecordIds.some((recordId) =>
get(
isRowSelectedComponentFamilyState.atomFamily({
instanceId,
familyKey: recordId,
}),
),
);
return isAnyRecordSelected;
},
});

View File

@ -4,6 +4,7 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldInput'; import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldInput';
import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldInput'; import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldInput';
@ -27,7 +28,7 @@ export const RecordTitleCellFieldInput = ({
onTab, onTab,
onClickOutside, onClickOutside,
}: RecordTitleCellFieldInputProps) => { }: RecordTitleCellFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext); const { fieldDefinition } = useContext(FieldContext);
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) { if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
throw new Error('Field definition is not a text or full name field'); throw new Error('Field definition is not a text or full name field');
@ -43,7 +44,7 @@ export const RecordTitleCellFieldInput = ({
onTab={onTab} onTab={onTab}
onShiftTab={onShiftTab} onShiftTab={onShiftTab}
sizeVariant={sizeVariant} sizeVariant={sizeVariant}
hotkeyScope={`record-title-cell-text-field-input-${recordId}`} hotkeyScope={DEFAULT_CELL_SCOPE.scope}
/> />
) : isFieldFullName(fieldDefinition) ? ( ) : isFieldFullName(fieldDefinition) ? (
<RecordTitleFullNameFieldInput <RecordTitleFullNameFieldInput
@ -53,7 +54,7 @@ export const RecordTitleCellFieldInput = ({
onTab={onTab} onTab={onTab}
onShiftTab={onShiftTab} onShiftTab={onShiftTab}
sizeVariant={sizeVariant} sizeVariant={sizeVariant}
hotkeyScope={`record-title-cell-full-name-field-input-${recordId}`} hotkeyScope={DEFAULT_CELL_SCOPE.scope}
/> />
) : null} ) : null}
</> </>

View File

@ -140,7 +140,7 @@ export const Dropdown = ({
dropdownHotkeyScope, dropdownHotkeyScope,
); );
toggleDropdown(); toggleDropdown(dropdownHotkeyScope);
onClickOutside?.(); onClickOutside?.();
}, },
[dropdownId, dropdownHotkeyScope, onClickOutside, toggleDropdown], [dropdownId, dropdownHotkeyScope, onClickOutside, toggleDropdown],

View File

@ -83,11 +83,11 @@ export const useDropdown = (dropdownId?: string) => {
], ],
); );
const toggleDropdown = () => { const toggleDropdown = (dropdownHotkeyScopeFromProps?: HotkeyScope) => {
if (isDropdownOpen) { if (isDropdownOpen) {
closeDropdown(); closeDropdown();
} else { } else {
openDropdown(); openDropdown(dropdownHotkeyScopeFromProps);
} }
}; };

View File

@ -20,11 +20,18 @@ export const usePreviousHotkeyScope = (memoizeKey = 'global') => {
.getValue(); .getValue();
if (!previousHotkeyScope) { if (!previousHotkeyScope) {
if (DEBUG_HOTKEY_SCOPE) {
logDebug(`DEBUG: no previous hotkey scope ${memoizeKey}`);
}
return; return;
} }
if (DEBUG_HOTKEY_SCOPE) { if (DEBUG_HOTKEY_SCOPE) {
logDebug('DEBUG: goBackToPreviousHotkeyScope', previousHotkeyScope); logDebug(
`DEBUG: goBackToPreviousHotkeyScope ${previousHotkeyScope.scope}`,
previousHotkeyScope,
);
} }
setHotkeyScope( setHotkeyScope(

View File

@ -1,9 +1,9 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback'; import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/hooks/useScopedHotkeyCallback';
import { logDebug } from '~/utils/logDebug';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { logDebug } from '~/utils/logDebug';
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants/DefaultHotkeysScopeCustomScopes'; import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants/DefaultHotkeysScopeCustomScopes';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState'; import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState'; import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
@ -84,7 +84,7 @@ export const useSetHotkeyScope = () =>
scopesToSet.push(newHotkeyScope.scope); scopesToSet.push(newHotkeyScope.scope);
if (DEBUG_HOTKEY_SCOPE) { if (DEBUG_HOTKEY_SCOPE) {
logDebug('DEBUG: set new hotkey scope', { logDebug(`DEBUG: set new hotkey scope : ${newHotkeyScope.scope}`, {
scopesToSet, scopesToSet,
newHotkeyScope, newHotkeyScope,
}); });

View File

@ -162,6 +162,21 @@ export const useListenClickOutside = <T extends Element>({
!isMouseDownInside && !isMouseDownInside &&
!isClickedOnExcluded; !isClickedOnExcluded;
if (CLICK_OUTSIDE_DEBUG_MODE) {
// eslint-disable-next-line no-console
console.log('click outside compare ref', {
listenerId,
shouldTrigger,
clickedOnAtLeastOneRef,
isMouseDownInside,
isListening,
hasMouseDownHappened,
isClickedOnExcluded,
enabled,
event,
});
}
if (shouldTrigger) { if (shouldTrigger) {
callback(event); callback(event);
} }