Fixed (#11482)
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:
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
export const getActionBarIdFromActionMenuId = (actionMenuId: string) => {
|
||||
return `action-bar-${actionMenuId}`;
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 />}</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -50,7 +50,7 @@ export const SelectFieldInput = ({
|
||||
onCancel?.();
|
||||
resetSelectedItem();
|
||||
},
|
||||
`select-field-input-${fieldDefinition.metadata.fieldName}`,
|
||||
DEFAULT_CELL_SCOPE.scope,
|
||||
[onCancel, resetSelectedItem],
|
||||
);
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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',
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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 />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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} />
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
@ -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}
|
||||
</>
|
||||
|
||||
@ -140,7 +140,7 @@ export const Dropdown = ({
|
||||
dropdownHotkeyScope,
|
||||
);
|
||||
|
||||
toggleDropdown();
|
||||
toggleDropdown(dropdownHotkeyScope);
|
||||
onClickOutside?.();
|
||||
},
|
||||
[dropdownId, dropdownHotkeyScope, onClickOutside, toggleDropdown],
|
||||
|
||||
@ -83,11 +83,11 @@ export const useDropdown = (dropdownId?: string) => {
|
||||
],
|
||||
);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
const toggleDropdown = (dropdownHotkeyScopeFromProps?: HotkeyScope) => {
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
openDropdown();
|
||||
openDropdown(dropdownHotkeyScopeFromProps);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user