Replace hotkey scopes by focus stack (Part 1 - Dropdowns and Side Panel) (#12673)

This PR is the first part of a refactoring aiming to deprecate the
hotkey scopes api in favor of the new focus stack api which is more
robust.

The refactored components in this PR are the dropdowns and the side
panel/command menu.

- Replaced `useScopedHotkeys` by `useHotkeysOnFocusedElement` for all
dropdown components, selectable lists and the command menu
- Introduced `focusId` for all dropdowns and created a common hotkey
scope `DropdownHotkeyScope` for backward compatibility
- Replaced `setHotkeyScopeAndMemorizePreviousScope` occurrences with
`usePushFocusItemToFocusStack` and `goBackToPreviousHotkeyScope` with
`removeFocusItemFromFocusStack`

Note: Test that the shorcuts and arrow key navigation still work
properly when interacting with dropdowns and the command menu.

Bugs that I have spotted during the QA but which are already present on
main:
- Icon picker select with arrow keys doesn’t work inside dropdowns
- Some dropdowns are not selectable with arrow keys (no selectable list)
- Dropdowns in dropdowns don’t reset the hotkey scope correctly when
closing
- The table click outside is not triggered after closing a table cell
and clicking outside of the table
This commit is contained in:
Raphaël Bosi
2025-06-19 14:53:18 +02:00
committed by GitHub
parent 6dd3a71497
commit cbc0d06a2f
155 changed files with 977 additions and 845 deletions

View File

@ -180,7 +180,6 @@ export const AdvancedFilterAddFilterRuleSelect = ({
</DropdownMenuItemsContainer>
</DropdownContent>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>

View File

@ -22,7 +22,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { FieldMetadataType } from 'twenty-shared/types';
type AdvancedFilterDropdownFilterInputProps = {
filterDropdownId?: string;
filterDropdownId: string;
recordFilter: RecordFilter;
};
@ -57,12 +57,15 @@ export const AdvancedFilterDropdownFilterInput = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilter.id} />
<ObjectFilterDropdownRecordSelect
recordFilterId={recordFilter.id}
dropdownId={filterDropdownId}
/>
</DropdownContent>
)}
{filterType === 'ACTOR' &&
(isActorSourceCompositeFilter ? (
<ObjectFilterDropdownSourceSelect />
<ObjectFilterDropdownSourceSelect dropdownId={filterDropdownId} />
) : (
<ObjectFilterDropdownTextInput />
))}
@ -70,7 +73,7 @@ export const AdvancedFilterDropdownFilterInput = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownOptionSelect />
<ObjectFilterDropdownOptionSelect focusId={filterDropdownId} />
</DropdownContent>
)}
{filterType === 'BOOLEAN' && <ObjectFilterDropdownBooleanSelect />}

View File

@ -34,7 +34,6 @@ export const AdvancedFilterFieldSelectDropdownButton = ({
recordFilterId={recordFilterId}
/>
}
dropdownHotkeyScope={{ scope: advancedFilterFieldSelectDropdownId }}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
dropdownPlacement="bottom-start"
/>

View File

@ -23,6 +23,7 @@ import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/uti
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -144,9 +145,10 @@ export const AdvancedFilterFieldSelectMenu = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<AdvancedFilterFieldSelectSearchInput />
<SelectableList
hotkeyScope={advancedFilterFieldSelectDropdownId}
focusId={advancedFilterFieldSelectDropdownId}
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{shouldShowVisibleFields && (
<>

View File

@ -65,7 +65,6 @@ export const AdvancedFilterRecordFilterGroupOptionsDropdown = ({
</DropdownMenuItemsContainer>
</DropdownContent>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={{ y: 2, x: 0 }}
dropdownPlacement="bottom-start"
/>

View File

@ -8,6 +8,7 @@ import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
@ -63,11 +64,12 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<SelectableList
hotkeyScope={dropdownId}
focusId={dropdownId}
selectableItemIdArray={operandsForFilterType.map(
(operand) => operand,
)}
selectableListInstanceId={dropdownId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{operandsForFilterType.map((filterOperand, index) => (
<SelectableListItem
@ -90,7 +92,6 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
</DropdownMenuItemsContainer>
</DropdownContent>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
dropdownPlacement="bottom-start"
/>

View File

@ -85,7 +85,6 @@ export const AdvancedFilterRecordFilterOptionsDropdown = ({
</DropdownMenuItemsContainer>
</DropdownContent>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
dropdownPlacement="bottom-start"
/>

View File

@ -18,6 +18,7 @@ import { CompositeFieldSubFieldName } from '@/settings/data-model/types/Composit
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@ -124,9 +125,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<SelectableList
hotkeyScope={advancedFilterFieldSelectDropdownId}
focusId={advancedFilterFieldSelectDropdownId}
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{compositeFieldTypeIsFilterableByAnySubField && (
<SelectableListItem

View File

@ -104,9 +104,11 @@ export const AdvancedFilterValueInput = ({
/>
}
dropdownComponents={
<AdvancedFilterDropdownFilterInput recordFilter={recordFilter} />
<AdvancedFilterDropdownFilterInput
recordFilter={recordFilter}
filterDropdownId={dropdownId}
/>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={dropdownContentOffset}
dropdownPlacement="bottom-start"
onClose={handleFilterValueDropdownClose}

View File

@ -13,10 +13,10 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
@ -44,7 +44,7 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
currentRecordFiltersComponentState,
);
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
@ -76,7 +76,17 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
fieldMetadataItem.type === 'RELATION' ||
fieldMetadataItem.type === 'SELECT'
) {
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
pushFocusItemToFocusStack({
focusId: fieldMetadataItem.id,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: fieldMetadataItem.id,
},
hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown,
},
memoizeKey: fieldMetadataItem.id,
});
}
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);

View File

@ -3,10 +3,10 @@ import styled from '@emotion/styled';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
import { useObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useObjectFilterDropdownFilterValue';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
@ -57,7 +57,8 @@ export const ObjectFilterDropdownBooleanSelect = () => {
<SelectableList
selectableListInstanceId="boolean-select"
selectableItemIdArray={options.map((option) => option.toString())}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
focusId="boolean-select"
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<DropdownMenuItemsContainer hasMaxHeight>
{options.map((option) => (

View File

@ -25,7 +25,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { isDefined } from 'twenty-shared/utils';
type ObjectFilterDropdownFilterInputProps = {
filterDropdownId?: string;
filterDropdownId: string;
recordFilterId?: string;
};
@ -113,7 +113,10 @@ export const ObjectFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect recordFilterId={recordFilterId} />
<ObjectFilterDropdownRecordSelect
recordFilterId={recordFilterId}
dropdownId={filterDropdownId}
/>
</>
)}
{filterType === 'ACTOR' && <ObjectFilterDropdownTextInput />}
@ -123,7 +126,7 @@ export const ObjectFilterDropdownFilterInput = ({
<>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownOptionSelect />
<ObjectFilterDropdownOptionSelect focusId={filterDropdownId} />
</>
)}
{filterType === 'BOOLEAN' && <ObjectFilterDropdownBooleanSelect />}

View File

@ -3,7 +3,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -47,9 +46,6 @@ export const ObjectFilterDropdownOperandDropdown = ({
</StyledDropdownMenuHeader>
}
dropdownComponents={<ObjectFilterDropdownOperandSelect />}
dropdownHotkeyScope={{
scope: FiltersHotkeyScope.ObjectFilterDropdownOperandDropdown,
}}
dropdownOffset={{ x: parseInt(theme.spacing(2), 10) }}
/>
</ClickOutsideListenerContext.Provider>

View File

@ -14,9 +14,9 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/ob
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards';
@ -30,7 +30,11 @@ type SelectOptionForFilter = FieldMetadataItemOption & {
isSelected: boolean;
};
export const ObjectFilterDropdownOptionSelect = () => {
export const ObjectFilterDropdownOptionSelect = ({
focusId,
}: {
focusId: string;
}) => {
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
@ -92,15 +96,16 @@ export const ObjectFilterDropdownOptionSelect = () => {
}
}, [selectedOptions, selectOptions]);
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
closeDropdown();
resetSelectedItem();
},
SingleRecordPickerHotkeyScope.SingleRecordPicker,
[closeDropdown, resetSelectedItem],
);
focusId,
scope: DropdownHotkeyScope.Dropdown,
dependencies: [closeDropdown, resetSelectedItem],
});
const handleMultipleOptionSelectChange = (
optionChanged: SelectOptionForFilter,
@ -148,7 +153,8 @@ export const ObjectFilterDropdownOptionSelect = () => {
<SelectableList
selectableListInstanceId={componentInstanceId}
selectableItemIdArray={objectRecordsIds}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
focusId={focusId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<DropdownMenuItemsContainer hasMaxHeight>
{showNoResult ? (

View File

@ -8,7 +8,6 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
@ -29,10 +28,12 @@ export const MAX_RECORDS_TO_DISPLAY = 3;
type ObjectFilterDropdownRecordSelectProps = {
recordFilterId?: string;
dropdownId: string;
};
export const ObjectFilterDropdownRecordSelect = ({
recordFilterId,
dropdownId,
}: ObjectFilterDropdownRecordSelectProps) => {
const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
@ -220,7 +221,7 @@ export const ObjectFilterDropdownRecordSelect = ({
)}
<MultipleSelectDropdown
selectableListId="object-filter-record-select-id"
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
focusId={dropdownId}
itemsToSelect={recordsToSelect}
filteredSelectedItems={filteredSelectedRecords}
selectedItems={selectedRecords}

View File

@ -3,7 +3,6 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@ -15,7 +14,11 @@ import { isDefined } from 'twenty-shared/utils';
export const EMPTY_FILTER_VALUE = '[]';
export const MAX_ITEMS_TO_DISPLAY = 3;
export const ObjectFilterDropdownSourceSelect = () => {
export const ObjectFilterDropdownSourceSelect = ({
dropdownId,
}: {
dropdownId: string;
}) => {
const objectFilterDropdownSearchInput = useRecoilComponentValueV2(
objectFilterDropdownSearchInputComponentState,
);
@ -78,7 +81,7 @@ export const ObjectFilterDropdownSourceSelect = () => {
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<MultipleSelectDropdown
selectableListId="object-filter-source-select-id"
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
focusId={dropdownId}
itemsToSelect={sourceTypes.filter(
(item) =>
!filteredSelectedItems.some((selected) => selected.id === item.id),

View File

@ -1,5 +0,0 @@
export enum FiltersHotkeyScope {
ObjectFilterDropdownButton = 'filter-dropdown-button',
ObjectSortDropdownButton = 'sort-dropdown-button',
ObjectFilterDropdownOperandDropdown = 'filter-dropdown-operand-dropdown',
}

View File

@ -7,7 +7,6 @@ import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dro
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal';
import { useRecordGroupReorderConfirmationModal } from '@/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@ -40,7 +39,6 @@ export const ObjectOptionsDropdown = ({
<>
<Dropdown
dropdownId={OBJECT_OPTIONS_DROPDOWN_ID}
dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<StyledHeaderDropdownButton isUnfolded={isDropdownOpen}>

View File

@ -4,12 +4,12 @@ import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hook
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@ -108,8 +108,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
{!!currentView && (
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem

View File

@ -2,11 +2,11 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
@ -53,8 +53,9 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}

View File

@ -1,20 +1,17 @@
import { Key } from 'ts-key-enum';
import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
@ -48,14 +45,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
(isDefined(currentView?.viewGroups) && currentView.viewGroups.length > 0) ||
currentView?.key !== 'INDEX';
useScopedHotkeys(
[Key.Escape],
() => {
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const { visibleBoardFields } = useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
@ -101,8 +90,9 @@ export const ObjectOptionsDropdownMenuContent = () => {
<DropdownMenuSeparator />
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem

View File

@ -4,12 +4,13 @@ import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropd
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { View } from '@/views/types/View';
import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useUpdateViewFromCurrentState } from '@/views/view-picker/hooks/useUpdateViewFromCurrentState';
import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState';
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
@ -75,17 +76,19 @@ export const ObjectOptionsDropdownMenuViewName = ({
const { updateViewFromCurrentState } = useUpdateViewFromCurrentState();
const [viewName, setViewName] = useState(currentView?.name);
useScopedHotkeys(
Key.Enter,
async () => {
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: async () => {
if (viewPickerIsPersisting) {
return;
}
await updateViewFromCurrentState();
},
ViewsHotkeyScope.ListDropdown,
);
focusId: VIEW_PICKER_DROPDOWN_ID,
scope: DropdownHotkeyScope.Dropdown,
dependencies: [viewPickerIsPersisting, updateViewFromCurrentState],
});
const handleIconChange = ({ iconKey }: { iconKey: string }) => {
setViewPickerIsDirty(true);

View File

@ -5,11 +5,11 @@ import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hook
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
@ -73,8 +73,9 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<SelectableListItem
itemId={RecordGroupSort.Manual}

View File

@ -9,12 +9,12 @@ import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-gr
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
@ -94,6 +94,8 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
'HideEmptyGroups',
];
const hiddenGroupsSelectableListId = `${OBJECT_OPTIONS_DROPDOWN_ID}-hidden-groups`;
return (
<DropdownContent>
<DropdownMenuHeader
@ -109,8 +111,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{currentView?.key !== 'INDEX' && (
<>
@ -175,9 +178,10 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<SelectableList
selectableListInstanceId={`${OBJECT_OPTIONS_DROPDOWN_ID}-hidden-groups`}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableListInstanceId={hiddenGroupsSelectableListId}
focusId={hiddenGroupsSelectableListId}
selectableItemIdArray={['HiddenGroups']}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<SelectableListItem
itemId="HiddenGroups"

View File

@ -6,7 +6,6 @@ import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/co
import { useCloseSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useCloseSortDropdown';
import { useResetRecordSortDropdownSearchInput } from '@/object-record/object-sort-dropdown/hooks/useResetRecordSortDropdownSearchInput';
import { useResetSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useResetSortDropdown';
import { useToggleSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useToggleSortDropdown';
import { isRecordSortDirectionDropdownMenuUnfoldedComponentState } from '@/object-record/object-sort-dropdown/states/isRecordSortDirectionDropdownMenuUnfoldedComponentState';
import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState';
import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState';
@ -25,6 +24,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
@ -86,11 +86,7 @@ export type ObjectSortDropdownButtonProps = {
hotkeyScope: HotkeyScope;
};
export const ObjectSortDropdownButton = ({
hotkeyScope,
}: ObjectSortDropdownButtonProps) => {
const { toggleSortDropdown } = useToggleSortDropdown();
export const ObjectSortDropdownButton = () => {
const { resetRecordSortDropdownSearchInput } =
useResetRecordSortDropdownSearchInput();
@ -162,15 +158,16 @@ export const ObjectSortDropdownButton = ({
const shouldShowSeparator =
visibleFieldMetadataItems.length > 0 && hiddenFieldMetadataItems.length > 0;
const handleButtonClick = () => {
toggleSortDropdown();
};
const handleDropdownButtonClose = () => {
resetRecordSortDropdownSearchInput();
resetSortDropdown();
};
const handleDropdownOpen = () => {
resetSortDropdown();
setSelectedItemId(selectableItemIdArray[0]);
};
const { closeSortDropdown } = useCloseSortDropdown();
const { upsertRecordSort } = useUpsertRecordSort();
@ -224,16 +221,10 @@ export const ObjectSortDropdownButton = ({
return (
<Dropdown
dropdownId={OBJECT_SORT_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
dropdownOffset={{ y: 8 }}
onOpen={handleDropdownOpen}
clickableComponent={
<StyledHeaderDropdownButton
onClick={() => {
handleButtonClick();
setSelectedItemId(selectableItemIdArray[0]);
}}
isUnfolded={isDropdownOpen}
>
<StyledHeaderDropdownButton isUnfolded={isDropdownOpen}>
<Trans>Sort</Trans>
</StyledHeaderDropdownButton>
}
@ -241,8 +232,9 @@ export const ObjectSortDropdownButton = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<SelectableList
selectableListInstanceId={OBJECT_SORT_DROPDOWN_ID}
hotkeyScope={hotkeyScope.scope}
selectableItemIdArray={selectableItemIdArray}
focusId={OBJECT_SORT_DROPDOWN_ID}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{isRecordSortDirectionMenuUnfolded && (
<StyledSelectedSortDirectionContainer>

View File

@ -9,7 +9,6 @@ import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/r
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
@ -155,7 +154,8 @@ export const RecordBoardCard = () => {
y: event.clientY,
});
openDropdown(actionMenuDropdownId, {
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
enableGlobalHotkeysWithModifiers: true,
enableGlobalHotkeysConflictingWithKeyboard: false,
});
};

View File

@ -5,7 +5,6 @@ import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from
import { RecordBoardColumnHeaderAggregateDropdownButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton';
import { AggregateDropdownContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent';
import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
import { RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import styled from '@emotion/styled';
@ -42,9 +41,6 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
<Dropdown
onClose={handleResetContent}
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: RecordBoardColumnHotkeyScope.ColumnHeader,
}}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<RecordBoardColumnHeaderAggregateDropdownButton

View File

@ -11,6 +11,7 @@ import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldM
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
@ -21,7 +22,6 @@ import { isDefined } from 'twenty-shared/utils';
import { VisibilityHidden } from 'twenty-ui/accessibility';
import { IconChevronDown } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
type FormMultiSelectFieldInputProps = {
label?: string;
@ -256,7 +256,7 @@ export const FormMultiSelectFieldInput = ({
selectableListComponentInstanceId={
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
hotkeyScope={hotkeyScope}
focusId={hotkeyScope}
options={options}
onCancel={onCancel}
onOptionSelected={onOptionSelected}

View File

@ -10,6 +10,7 @@ import { singleRecordPickerSelectedIdComponentState } from '@/object-record/reco
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
@ -18,7 +19,6 @@ import styled from '@emotion/styled';
import { useCallback, useId } from 'react';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)<{
readonly?: boolean;
@ -189,6 +189,7 @@ export const FormSingleRecordPicker = ({
}
dropdownComponents={
<SingleRecordPicker
focusId={dropdownId}
componentInstanceId={dropdownId}
EmptyIcon={IconForbid}
emptyLabel={'No ' + objectNameSingular}
@ -199,7 +200,6 @@ export const FormSingleRecordPicker = ({
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
)}
{isDefined(VariablePicker) && !disabled && (

View File

@ -7,6 +7,7 @@ import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTa
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldMetadata,
@ -15,11 +16,13 @@ import {
} from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@ -31,7 +34,7 @@ export const useOpenFieldInputEditMode = () => {
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const openFieldInput = useRecoilCallback(
({ snapshot }) =>
@ -72,7 +75,10 @@ export const useOpenFieldInputEditMode = () => {
});
openActivityTargetCellEditMode({
recordPickerInstanceId: `relation-from-many-field-input-${recordId}`,
recordPickerInstanceId: getRelationFromManyFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
activityTargetObjectRecords,
});
return;
@ -103,9 +109,22 @@ export const useOpenFieldInputEditMode = () => {
}
}
setHotkeyScopeAndMemorizePreviousScope({
scope: DEFAULT_CELL_SCOPE.scope,
customScopes: DEFAULT_CELL_SCOPE.customScopes,
pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId(
recordId,
fieldDefinition.metadata.fieldName,
),
component: {
type: FocusComponentType.OPEN_FIELD_INPUT,
instanceId: getFieldInputInstanceId(
recordId,
fieldDefinition.metadata.fieldName,
),
},
hotkeyScope: {
scope: DEFAULT_CELL_SCOPE.scope,
customScopes: DEFAULT_CELL_SCOPE.customScopes,
},
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
});
},
@ -113,7 +132,7 @@ export const useOpenFieldInputEditMode = () => {
openActivityTargetCellEditMode,
openRelationFromManyFieldInput,
openRelationToOneFieldInput,
setHotkeyScopeAndMemorizePreviousScope,
pushFocusItemToFocusStack,
],
);

View File

@ -39,6 +39,7 @@ export const useMultiSelectField = () => {
const draftValue = useRecoilValue(getDraftValueSelector());
return {
recordId,
fieldDefinition,
persistField,
fieldValues: fieldMultiSelectValues,

View File

@ -34,6 +34,7 @@ export const useSelectField = () => {
const draftValue = useRecoilValue(getDraftValueSelector());
return {
recordId,
fieldDefinition,
persistField,
fieldValue: fieldSelectValue,

View File

@ -1,6 +1,6 @@
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
type MultiSelectFieldInputProps = {
@ -10,14 +10,18 @@ type MultiSelectFieldInputProps = {
export const MultiSelectFieldInput = ({
onCancel,
}: MultiSelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValues } = useMultiSelectField();
const { persistField, fieldDefinition, fieldValues, recordId } =
useMultiSelectField();
return (
<MultiSelectInput
selectableListComponentInstanceId={
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
focusId={getFieldInputInstanceId(
recordId,
fieldDefinition.metadata.fieldName,
)}
options={fieldDefinition.metadata.options}
onCancel={onCancel}
onOptionSelected={persistField}

View File

@ -10,6 +10,7 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
@ -25,7 +26,10 @@ export const RelationFromManyFieldInput = ({
onSubmit,
}: RelationFromManyFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
const recordPickerInstanceId = getRelationFromManyFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const { updateRelation } = useUpdateRelationFromManyFieldInput();
const fieldName = fieldDefinition.metadata.fieldName;
@ -84,6 +88,7 @@ export const RelationFromManyFieldInput = ({
return (
<MultipleRecordPicker
focusId={recordPickerInstanceId}
componentInstanceId={recordPickerInstanceId}
onSubmit={handleSubmit}
onChange={(morphItem) => {

View File

@ -3,6 +3,7 @@ import { useRelationField } from '../../hooks/useRelationField';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
@ -25,7 +26,10 @@ export const RelationToOneFieldInput = ({
const persistField = usePersistField();
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldDefinition.metadata.fieldName}`;
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const handleRecordSelected = (
selectedRecord: SingleRecordPickerRecord | null | undefined,
@ -64,6 +68,7 @@ export const RelationToOneFieldInput = ({
return (
<SingleRecordPicker
focusId={recordPickerInstanceId}
componentInstanceId={recordPickerInstanceId}
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}

View File

@ -2,6 +2,7 @@ import { useClearField } from '@/object-record/record-field/hooks/useClearField'
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
@ -20,7 +21,8 @@ export const SelectFieldInput = ({
onSubmit,
onCancel,
}: SelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValue } = useSelectField();
const { persistField, fieldDefinition, fieldValue, recordId } =
useSelectField();
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
@ -65,7 +67,10 @@ export const SelectFieldInput = ({
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
selectableItemIdArray={optionIds}
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
focusId={getFieldInputInstanceId(
recordId,
fieldDefinition.metadata.fieldName,
)}
onEnter={(itemId) => {
const option = filteredOptions.find(
(option) => option.value === itemId,

View File

@ -18,9 +18,10 @@ import {
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
@ -71,7 +72,7 @@ const RelationManyFieldInputWithContext = () => {
useEffect(() => {
setRecordStoreFieldValue([]);
setHotKeyScope(MultipleRecordPickerHotkeyScope.MultipleRecordPicker);
setHotKeyScope(DropdownHotkeyScope.Dropdown);
openFieldInput({
fieldDefinition,
recordId: 'recordId',
@ -87,7 +88,10 @@ const RelationManyFieldInputWithContext = () => {
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'relation-from-many-field-record-id-people',
instanceId: getRelationFromManyFieldInputInstanceId({
recordId: 'recordId',
fieldName: 'people',
}),
}}
>
<FieldContext.Provider

View File

@ -5,7 +5,6 @@ import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -17,9 +16,12 @@ import {
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
@ -62,11 +64,30 @@ const RelationToOneFieldInputWithContext = ({
onSubmit,
onCancel,
}: RelationToOneFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
useEffect(() => {
setHotKeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
}, [setHotKeyScope]);
pushFocusItemToFocusStack({
focusId: getRelationToOneFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
component: {
type: FocusComponentType.DROPDOWN,
instanceId: getRelationToOneFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
},
hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown,
},
memoizeKey: getRelationToOneFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
});
}, [pushFocusItemToFocusStack]);
return (
<div>

View File

@ -1,4 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getRelationFromManyFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationFromManyFieldInputInstanceId';
import {
FieldRelationFromManyValue,
FieldRelationValue,
@ -6,17 +7,18 @@ import {
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil';
export const useOpenRelationFromManyFieldInput = () => {
const { performSearch } = useMultipleRecordPickerPerformSearch();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const openRelationFromManyFieldInput = useRecoilCallback(
({ set, snapshot }) =>
@ -29,7 +31,10 @@ export const useOpenRelationFromManyFieldInput = () => {
objectNameSingular: string;
recordId: string;
}) => {
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
const recordPickerInstanceId = getRelationFromManyFieldInputInstanceId({
recordId,
fieldName,
});
const fieldValue = snapshot
.getLoadable<FieldRelationValue<FieldRelationFromManyValue>>(
@ -88,11 +93,19 @@ export const useOpenRelationFromManyFieldInput = () => {
forcePickableMorphItems: pickableMorphItems,
});
setHotkeyScopeAndMemorizePreviousScope({
scope: MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
pushFocusItemToFocusStack({
focusId: recordPickerInstanceId,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: recordPickerInstanceId,
},
hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown,
},
memoizeKey: recordPickerInstanceId,
});
},
[performSearch, setHotkeyScopeAndMemorizePreviousScope],
[performSearch, pushFocusItemToFocusStack],
);
return { openRelationFromManyFieldInput };

View File

@ -1,21 +1,26 @@
import { getRelationToOneFieldInputInstanceId } from '@/object-record/record-field/meta-types/input/utils/getRelationToOneFieldInputInstanceId';
import {
FieldRelationToOneValue,
FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useOpenRelationToOneFieldInput = () => {
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const openRelationToOneFieldInput = useRecoilCallback(
({ set, snapshot }) =>
({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldName}`;
const recordPickerInstanceId = getRelationToOneFieldInputInstanceId({
recordId,
fieldName,
});
const fieldValue = snapshot
.getLoadable<FieldRelationValue<FieldRelationToOneValue>>(
recordStoreFamilySelector({
@ -34,11 +39,18 @@ export const useOpenRelationToOneFieldInput = () => {
);
}
setHotkeyScopeAndMemorizePreviousScope({
scope: SingleRecordPickerHotkeyScope.SingleRecordPicker,
pushFocusItemToFocusStack({
focusId: recordPickerInstanceId,
component: {
type: FocusComponentType.OPEN_FIELD_INPUT,
instanceId: recordPickerInstanceId,
},
// TODO: Remove this once we've fully migrated away from hotkey scopes
hotkeyScope: { scope: DropdownHotkeyScope.Dropdown },
memoizeKey: recordPickerInstanceId,
});
},
[setHotkeyScopeAndMemorizePreviousScope],
[pushFocusItemToFocusStack],
);
return { openRelationToOneFieldInput };

View File

@ -1,3 +0,0 @@
export enum RelationPickerHotkeyScope {
AddNew = 'add-new',
}

View File

@ -0,0 +1,9 @@
export const getRelationFromManyFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}): string => {
return `relation-from-many-field-input-${recordId}-${fieldName}`;
};

View File

@ -0,0 +1,9 @@
export const getRelationToOneFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}): string => {
return `relation-to-one-field-input-${recordId}-${fieldName}`;
};

View File

@ -0,0 +1,6 @@
export const getFieldInputInstanceId = (
recordId: string,
fieldName: string,
) => {
return `${recordId}-${fieldName}`;
};

View File

@ -4,7 +4,6 @@ import { MultipleRecordPickerSearchInput } from '@/object-record/record-picker/m
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
@ -12,10 +11,11 @@ import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObj
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import styled from '@emotion/styled';
import { useRef } from 'react';
@ -36,6 +36,7 @@ type MultipleRecordPickerProps = {
layoutDirection?: RecordPickerLayoutDirection;
componentInstanceId: string;
onClickOutside: () => void;
focusId: string;
};
export const MultipleRecordPicker = ({
@ -45,6 +46,7 @@ export const MultipleRecordPicker = ({
onClickOutside,
layoutDirection = 'search-bar-on-bottom',
componentInstanceId,
focusId,
}: MultipleRecordPickerProps) => {
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
@ -94,14 +96,15 @@ export const MultipleRecordPicker = ({
resetState();
};
useScopedHotkeys(
Key.Escape,
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
handleSubmit();
},
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
[handleSubmit],
);
focusId,
scope: DropdownHotkeyScope.Dropdown,
dependencies: [handleSubmit],
});
const containerRef = useRef<HTMLDivElement>(null);
@ -140,13 +143,19 @@ export const MultipleRecordPicker = ({
{layoutDirection === 'search-bar-on-bottom' && (
<>
{createNewButtonSection}
<MultipleRecordPickerItemsDisplay onChange={onChange} />
<MultipleRecordPickerItemsDisplay
onChange={onChange}
focusId={focusId}
/>
</>
)}
<MultipleRecordPickerSearchInput />
{layoutDirection === 'search-bar-on-top' && (
<>
<MultipleRecordPickerItemsDisplay onChange={onChange} />
<MultipleRecordPickerItemsDisplay
onChange={onChange}
focusId={focusId}
/>
{createNewButtonSection}
</>
)}

View File

@ -10,8 +10,10 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
export const MultipleRecordPickerItemsDisplay = ({
onChange,
focusId,
}: {
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
focusId: string;
}) => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext,
@ -33,7 +35,7 @@ export const MultipleRecordPickerItemsDisplay = ({
{isLoading && itemsLength === 0 ? (
<DropdownMenuSkeletonItem />
) : (
<MultipleRecordPickerMenuItems onChange={onChange} />
<MultipleRecordPickerMenuItems onChange={onChange} focusId={focusId} />
)}
<DropdownMenuSeparator />
</>

View File

@ -6,10 +6,10 @@ import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/mult
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
@ -25,10 +25,12 @@ export const StyledSelectableItem = styled(SelectableListItem)`
type MultipleRecordPickerMenuItemsProps = {
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
focusId: string;
};
export const MultipleRecordPickerMenuItems = ({
onChange,
focusId,
}: MultipleRecordPickerMenuItemsProps) => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext,
@ -85,7 +87,8 @@ export const MultipleRecordPickerMenuItems = ({
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={pickableRecordIds}
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
focusId={focusId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
{pickableRecordIds.map((recordId) => {
return (

View File

@ -1,3 +0,0 @@
export enum MultipleRecordPickerHotkeyScope {
MultipleRecordPicker = 'multiple-record-picker',
}

View File

@ -28,6 +28,7 @@ export const SingleRecordPicker = ({
componentInstanceId,
layoutDirection,
dropdownWidth,
focusId,
}: SingleRecordPickerProps) => {
const containerRef = useRef<HTMLDivElement>(null);
@ -71,6 +72,7 @@ export const SingleRecordPicker = ({
>
<DropdownContent ref={containerRef} widthInPixels={dropdownWidth}>
<SingleRecordPickerMenuItemsWithSearch
focusId={focusId}
{...{
EmptyIcon,
emptyLabel,

View File

@ -4,16 +4,16 @@ import { Key } from 'ts-key-enum';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
@ -29,7 +29,7 @@ export type SingleRecordPickerMenuItemsProps = {
onCancel?: () => void;
onRecordSelected: (entity?: SingleRecordPickerRecord) => void;
selectedRecord?: SingleRecordPickerRecord;
hotkeyScope?: string;
focusId: string;
};
export const SingleRecordPickerMenuItems = ({
@ -40,7 +40,7 @@ export const SingleRecordPickerMenuItems = ({
onCancel,
onRecordSelected,
selectedRecord,
hotkeyScope = SingleRecordPickerHotkeyScope.SingleRecordPicker,
focusId,
}: SingleRecordPickerMenuItemsProps) => {
const selectNone = emptyLabel
? {
@ -77,15 +77,16 @@ export const SingleRecordPickerMenuItems = ({
'select-none',
);
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: Key.Escape,
callback: () => {
resetSelectedItem();
onCancel?.();
},
hotkeyScope,
[onCancel, resetSelectedItem],
);
focusId,
scope: DropdownHotkeyScope.Dropdown,
dependencies: [onCancel, resetSelectedItem],
});
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
const [selectedRecordId, setSelectedRecordId] = useRecoilComponentStateV2(
@ -96,7 +97,8 @@ export const SingleRecordPickerMenuItems = ({
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
hotkeyScope={DropdownHotkeyScope.Dropdown}
focusId={focusId}
>
{loading ? (
<DropdownMenuSkeletonItem />

View File

@ -26,6 +26,7 @@ export type SingleRecordPickerMenuItemsWithSearchProps = {
objectNameSingular: string;
recordPickerInstanceId?: string;
layoutDirection?: RecordPickerLayoutDirection;
focusId: string;
} & Pick<
SingleRecordPickerMenuItemsProps,
| 'EmptyIcon'
@ -44,6 +45,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
onRecordSelected,
objectNameSingular,
layoutDirection = 'search-bar-on-top',
focusId,
}: SingleRecordPickerMenuItemsWithSearchProps) => {
const { handleSearchFilterChange } = useSingleRecordPickerSearch();
@ -96,9 +98,11 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItemsContainer hasMaxHeight>
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
<SingleRecordPickerMenuItems
focusId={focusId}
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}
@ -123,6 +127,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<SingleRecordPickerMenuItems
focusId={focusId}
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}

View File

@ -1,3 +0,0 @@
export enum SingleRecordPickerHotkeyScope {
SingleRecordPicker = 'single-record-picker',
}

View File

@ -285,7 +285,6 @@ export const RecordDetailRelationRecordsListItem = ({
</DropdownMenuItemsContainer>
</DropdownContent>
}
dropdownHotkeyScope={{ scope: dropdownScopeId }}
/>
</DropdownScope>
)}

View File

@ -216,10 +216,10 @@ export const RecordDetailRelationSectionDropdown = ({
accent="tertiary"
/>
}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownComponents={
isToOneObject ? (
<SingleRecordPicker
focusId={dropdownId}
componentInstanceId={dropdownId}
EmptyIcon={IconForbid}
onRecordSelected={handleRelationPickerEntitySelected}
@ -235,6 +235,7 @@ export const RecordDetailRelationSectionDropdown = ({
/>
) : (
<MultipleRecordPicker
focusId={dropdownId}
componentInstanceId={dropdownId}
onCreate={() => {
closeDropdown();

View File

@ -8,6 +8,7 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useResetFocusStack } from '@/ui/utilities/focus/hooks/useResetFocusStack';
export const useCloseRecordTableCellInGroup = () => {
const { recordTableId } = useRecordTableContextOrThrow();
@ -22,11 +23,17 @@ export const useCloseRecordTableCellInGroup = () => {
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const { resetFocusStack } = useResetFocusStack();
const closeTableCellInGroup = useRecoilCallback(
() => () => {
toggleClickOutside(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
// TODO: Remove this once we've fully migrated away from hotkey scopes
resetFocusStack();
setHotkeyScope(TableHotkeyScope.TableFocus, {
goto: true,
keyboardShortcutMenu: true,
@ -35,6 +42,7 @@ export const useCloseRecordTableCellInGroup = () => {
},
[
closeCurrentTableCellInEditMode,
resetFocusStack,
setDragSelectionStartEnabled,
setHotkeyScope,
toggleClickOutside,

View File

@ -6,6 +6,7 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useResetFocusStack } from '@/ui/utilities/focus/hooks/useResetFocusStack';
import { useCallback } from 'react';
export const useCloseRecordTableCellNoGroup = () => {
@ -22,10 +23,16 @@ export const useCloseRecordTableCellNoGroup = () => {
const closeCurrentTableCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const { resetFocusStack } = useResetFocusStack();
const closeTableCellNoGroup = useCallback(() => {
toggleClickOutside(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
// TODO: Remove this once we've fully migrated away from hotkey scopes
resetFocusStack();
setHotkeyScope(TableHotkeyScope.TableFocus, {
goto: true,
keyboardShortcutMenu: true,
@ -33,6 +40,7 @@ export const useCloseRecordTableCellNoGroup = () => {
});
}, [
closeCurrentTableCellInEditMode,
resetFocusStack,
setDragSelectionStartEnabled,
setHotkeyScope,
toggleClickOutside,

View File

@ -2,7 +2,6 @@ import { useRecoilCallback } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
@ -58,9 +57,7 @@ export const useTriggerActionMenuDropdown = ({
closeCommandMenu();
openDropdown({
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
});
openDropdown();
},
[
recordIndexActionMenuDropdownPositionState,

View File

@ -76,7 +76,6 @@ export const RecordTableColumnFooterWithDropdown = ({
}
dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{ scope: dropdownId }}
/>
);
};

View File

@ -35,7 +35,6 @@ export const RecordTableColumnHeadWithDropdown = ({
dropdownComponents={<RecordTableColumnHeadDropdownMenu column={column} />}
dropdownOffset={{ x: -1 }}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }}
/>
);
};

View File

@ -50,9 +50,6 @@ const StyledDropdownContainer = styled.div`
cursor: pointer;
`;
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
'hidden-table-columns-dropdown-hotkey-scope-id';
export const RecordTableHeaderLastColumn = () => {
const theme = useTheme();
@ -89,9 +86,6 @@ export const RecordTableHeaderLastColumn = () => {
}
dropdownComponents={<RecordTableHeaderPlusButtonContent />}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
}}
/>
</StyledDropdownContainer>
</StyledPlusIconHeaderCell>

View File

@ -3,19 +3,20 @@ import { Key } from 'ts-key-enum';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Avatar } from 'twenty-ui/display';
import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
export const MultipleSelectDropdown = ({
selectableListId,
hotkeyScope,
focusId,
itemsToSelect,
loadingItems,
filteredSelectedItems,
@ -23,7 +24,7 @@ export const MultipleSelectDropdown = ({
searchFilter,
}: {
selectableListId: string;
hotkeyScope: string;
focusId: string;
itemsToSelect: SelectableItem[];
filteredSelectedItems: SelectableItem[];
selectedItems: SelectableItem[];
@ -61,15 +62,16 @@ export const MultipleSelectDropdown = ({
...(itemsToSelect ?? []),
];
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
closeDropdown();
resetSelectedItem();
},
hotkeyScope,
[closeDropdown, resetSelectedItem],
);
focusId,
scope: DropdownHotkeyScope.Dropdown,
dependencies: [closeDropdown, resetSelectedItem],
});
const showNoResult =
itemsToSelect?.length === 0 &&
@ -83,7 +85,8 @@ export const MultipleSelectDropdown = ({
<SelectableList
selectableListInstanceId={selectableListId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
focusId={focusId}
hotkeyScope={DropdownHotkeyScope.Dropdown}
>
<DropdownMenuItemsContainer hasMaxHeight>
{itemsInDropdown?.map((item) => {