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 { 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 { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
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 { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
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 { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
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;
`;
import { AnimatePresence } from 'framer-motion';
import { useRecoilValue } from 'recoil';
export const CommandMenuContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
const { closeCommandMenu } = useCommandMenu();
const { commandMenuCloseAnimationCompleteCleanup } =
useCommandMenuCloseAnimationCompleteCleanup();
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(
contextStoreCurrentObjectMetadataItemIdComponentState,
COMMAND_MENU_COMPONENT_INSTANCE_ID,
@ -129,18 +67,9 @@ export const CommandMenuContainer = ({
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
{isCommandMenuOpened && (
<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 }}
>
<CommandMenuOpenContainer>
{children}
</StyledCommandMenu>
</CommandMenuOpenContainer>
)}
</AnimatePresence>
</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 { Key } from 'ts-key-enum';
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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useKeyboardShortcutMenu } from '../hooks/useKeyboardShortcutMenu';
import { isKeyboardShortcutMenuOpenedState } from '../states/isKeyboardShortcutMenuOpenedState';
import { KeyboardMenuDialog } from './KeyboardShortcutMenuDialog';
import { KeyboardMenuGroup } from './KeyboardShortcutMenuGroup';
import { KeyboardMenuItem } from './KeyboardShortcutMenuItem';
import { KeyboardShortcutMenuOpenContent } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuOpenContent';
export const KeyboardShortcutMenu = () => {
const { toggleKeyboardShortcutMenu, closeKeyboardShortcutMenu } =
useKeyboardShortcutMenu();
const { toggleKeyboardShortcutMenu } = useKeyboardShortcutMenu();
const isKeyboardShortcutMenuOpened = useRecoilValue(
isKeyboardShortcutMenuOpenedState,
);
@ -32,31 +26,7 @@ export const KeyboardShortcutMenu = () => {
[toggleKeyboardShortcutMenu],
);
useScopedHotkeys(
[Key.Escape],
() => {
closeKeyboardShortcutMenu();
},
AppHotkeyScope.KeyboardShortcutMenuOpen,
[closeKeyboardShortcutMenu],
);
return (
<>
{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>
)}
</>
<>{isKeyboardShortcutMenuOpened && <KeyboardShortcutMenuOpenContent />}</>
);
};

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 { useContext, useRef } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
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(Key.Escape, resetRecordSelection, TableHotkeyScope.Table);
const setIsRemoveSortingModalOpen = useSetRecoilState(
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 { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
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 { isDefined } from 'twenty-shared/utils';
@ -30,7 +30,7 @@ export const useOpenFieldInputEditMode = () => {
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
const setHotkeyScope = useSetHotkeyScope();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const openFieldInput = useRecoilCallback(
({ snapshot }) =>
@ -102,7 +102,7 @@ export const useOpenFieldInputEditMode = () => {
}
}
setHotkeyScope(
setHotkeyScopeAndMemorizePreviousScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
@ -111,7 +111,7 @@ export const useOpenFieldInputEditMode = () => {
openActivityTargetCellEditMode,
openRelationFromManyFieldInput,
openRelationToOneFieldInput,
setHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
],
);

View File

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

View File

@ -50,7 +50,7 @@ export const SelectFieldInput = ({
onCancel?.();
resetSelectedItem();
},
`select-field-input-${fieldDefinition.metadata.fieldName}`,
DEFAULT_CELL_SCOPE.scope,
[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 { 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 { useRecoilCallback } from 'recoil';
import { useIcons } from 'twenty-ui/display';
@ -80,9 +80,11 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope.scope !== InlineCellHotkeyScope.InlineCell) {
if (hotkeyScope.scope !== DEFAULT_CELL_SCOPE.scope) {
return;
}
event.stopImmediatePropagation();
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 { 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 {
hasRecordGroups: boolean;
@ -10,13 +15,29 @@ export interface RecordTableBodyEffectsWrapperProps {
export const RecordTableBodyEffectsWrapper = ({
hasRecordGroups,
tableBodyRef,
}: RecordTableBodyEffectsWrapperProps) => (
<>
{hasRecordGroups ? (
<RecordTableRecordGroupBodyEffects />
) : (
<RecordTableNoRecordGroupBodyEffect />
)}
<RecordTableBodyUnselectEffect tableBodyRef={tableBodyRef} />
</>
);
}: RecordTableBodyEffectsWrapperProps) => {
const isAtLeastOneRecordSelected = useRecoilComponentValueV2(
isAtLeastOneTableRowSelectedSelector,
);
const isSoftFocusActiveState = useRecoilComponentValueV2(
isSoftFocusActiveComponentState,
);
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 { 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 { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
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`
display: flex;
@ -39,10 +37,9 @@ export const RecordTableWithWrappers = ({
recordTableId,
viewBarId,
}: RecordTableWithWrappersProps) => {
const { resetTableRowSelection, selectAllRows, setHasUserSelectedAllRows } =
useRecordTable({
recordTableId,
});
const { selectAllRows, setHasUserSelectedAllRows } = useRecordTable({
recordTableId,
});
const handleSelectAllRows = () => {
setHasUserSelectedAllRows(true);
@ -55,8 +52,6 @@ export const RecordTableWithWrappers = ({
TableHotkeyScope.Table,
);
useScopedHotkeys(Key.Escape, resetTableRowSelection, TableHotkeyScope.Table);
const { saveViewFields } = useSaveCurrentViewFields();
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 { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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';
type RecordTableBodyUnselectEffectProps = {
type RecordTableBodySoftFocusClickOutsideEffectProps = {
tableBodyRef: React.RefObject<HTMLDivElement>;
};
export const RecordTableBodyUnselectEffect = ({
export const RecordTableBodySoftFocusClickOutsideEffect = ({
tableBodyRef,
}: RecordTableBodyUnselectEffectProps) => {
}: RecordTableBodySoftFocusClickOutsideEffectProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const leaveTableFocus = useLeaveTableFocus(recordTableId);
const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({
recordTableId,
});
useMapKeyboardToSoftFocus();
useScopedHotkeys(
[Key.Escape],
() => {
resetTableRowSelection();
},
TableHotkeyScope.Table,
);
useListenClickOutside({
excludeClassNames: [
'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 { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
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 { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
export const RecordTableCellDisplayMode = ({
children,
softFocus,
}: React.PropsWithChildren<{ softFocus?: boolean }>) => {
const { recordId } = useContext(FieldContext);
}: React.PropsWithChildren) => {
const { recordId, isReadOnly } = useContext(FieldContext);
const { columnIndex, hasSoftFocus } = useContext(RecordTableCellContext);
const isMobile = useIsMobile();
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) => {
onActionMenuDropdownOpened(event, recordId);
};
const handleClick = () => {
if (!isFieldInputOnly && !isReadOnly) {
openTableCell();
}
};
return (
<RecordTableCellDisplayContainer
softFocus={softFocus}
onContextMenu={handleActionMenuDropdown}
>
{children}
</RecordTableCellDisplayContainer>
<>
<RecordTableCellDisplayContainer
softFocus={hasSoftFocus}
onContextMenu={handleActionMenuDropdown}
onClick={handleClick}
>
{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 { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
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 { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldInput';
@ -27,7 +28,7 @@ export const RecordTitleCellFieldInput = ({
onTab,
onClickOutside,
}: RecordTitleCellFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const { fieldDefinition } = useContext(FieldContext);
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
throw new Error('Field definition is not a text or full name field');
@ -43,7 +44,7 @@ export const RecordTitleCellFieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
sizeVariant={sizeVariant}
hotkeyScope={`record-title-cell-text-field-input-${recordId}`}
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
/>
) : isFieldFullName(fieldDefinition) ? (
<RecordTitleFullNameFieldInput
@ -53,7 +54,7 @@ export const RecordTitleCellFieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
sizeVariant={sizeVariant}
hotkeyScope={`record-title-cell-full-name-field-input-${recordId}`}
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
/>
) : null}
</>

View File

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

View File

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

View File

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

View File

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

View File

@ -162,6 +162,21 @@ export const useListenClickOutside = <T extends Element>({
!isMouseDownInside &&
!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) {
callback(event);
}