512 Ability to navigate dropdown menus with keyboard (#11735)

# Ability to navigate dropdown menus with keyboard

The aim of this PR is to improve accessibility by allowing the user to
navigate inside the dropdown menus with the keyboard.
This PR refactors the `SelectableList` and `SelectableListItem`
components to move the Enter event handling responsibility from
`SelectableList` to the individual `SelectableListItem` components.
Closes [512](https://github.com/twentyhq/core-team-issues/issues/512)

## Key Changes:
- All dropdowns are now navigable with arrow keys

## Technical Implementation:
- Each `SelectableListItem` now has direct access to its own `Enter` key
handler, improving component encapsulation
- Removed the central `Enter` key handler logic from `SelectableList`
- Added `SelectableList` and `SelectableListItem` to all `Dropdown`
components inside the app
- Updated all component implementations to adapt to the new pattern:
  - Action menu components (`ActionDropdownItem`, `ActionListItem`)
  - Command menu components
  - Object filter, sort and options dropdowns
  - Record picker components
  - Select components

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-04-25 18:55:39 +02:00
committed by GitHub
parent 0b1b81429e
commit f201091c68
61 changed files with 1196 additions and 762 deletions

View File

@ -1,17 +1,14 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
@ -22,7 +19,6 @@ import { ObjectFilterDropdownFilterSelectMenuItemV2 } from '@/object-record/obje
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
@ -37,8 +33,10 @@ export const AdvancedFilterFieldSelectMenu = ({
}: AdvancedFilterFieldSelectMenuProps) => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const { closeAdvancedFilterFieldSelectDropdown } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const {
closeAdvancedFilterFieldSelectDropdown,
advancedFilterFieldSelectDropdownId,
} = useAdvancedFilterFieldSelectDropdown(recordFilterId);
const [objectFilterDropdownSearchInput] = useRecoilComponentStateV2(
objectFilterDropdownSearchInputComponentState,
@ -76,12 +74,10 @@ export const AdvancedFilterFieldSelectMenu = ({
(fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id),
);
const selectableFieldMetadataItemIds = filterableFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
const { resetSelectedItem } = useSelectableList(
advancedFilterFieldSelectDropdownId,
);
const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID);
const { selectFieldUsedInAdvancedFilterDropdown } =
useSelectFieldUsedInAdvancedFilterDropdown();
@ -98,18 +94,6 @@ export const AdvancedFilterFieldSelectMenu = ({
fieldMetadataItemIdUsedInDropdownComponentState,
);
const handleEnter = (fieldMetadataItemId: string) => {
const selectedFieldMetadataItem = filterableFieldMetadataItems.find(
(fieldMetadataItem) => fieldMetadataItem.id === fieldMetadataItemId,
);
if (!isDefined(selectedFieldMetadataItem)) {
return;
}
handleFieldMetadataItemSelect(selectedFieldMetadataItem);
};
const handleFieldMetadataItemSelect = (
selectedFieldMetadataItem: FieldMetadataItem,
) => {
@ -138,41 +122,55 @@ export const AdvancedFilterFieldSelectMenu = ({
visibleColumnsFieldMetadataItems.length > 0 &&
hiddenColumnsFieldMetadataItems.length > 0;
const selectableItemIdArray = [
...visibleColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
...hiddenColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
];
return (
<>
<AdvancedFilterFieldSelectSearchInput />
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableFieldMetadataItemIds}
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
hotkeyScope={advancedFilterFieldSelectDropdownId}
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
>
<DropdownMenuItemsContainer>
{visibleColumnsFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<SelectableItem
<SelectableListItem
itemId={visibleFieldMetadataItem.id}
key={`visible-select-filter-${index}`}
onEnter={() => {
handleFieldMetadataItemSelect(visibleFieldMetadataItem);
}}
>
<ObjectFilterDropdownFilterSelectMenuItemV2
fieldMetadataItemToSelect={visibleFieldMetadataItem}
onClick={handleFieldMetadataItemSelect}
/>
</SelectableItem>
</SelectableListItem>
),
)}
{shouldShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsFieldMetadataItems.map(
(hiddenFieldMetadataItem, index) => (
<SelectableItem
<SelectableListItem
itemId={hiddenFieldMetadataItem.id}
key={`hidden-select-filter-${index}`}
onEnter={() => {
handleFieldMetadataItemSelect(hiddenFieldMetadataItem);
}}
>
<ObjectFilterDropdownFilterSelectMenuItemV2
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
onClick={handleFieldMetadataItemSelect}
/>
</SelectableItem>
</SelectableListItem>
),
)}
</DropdownMenuItemsContainer>

View File

@ -11,6 +11,9 @@ import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
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 { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled';
@ -85,6 +88,11 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
})
: [];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
dropdownId,
);
if (isDisabled === true) {
return (
<SelectControl
@ -115,15 +123,31 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
}
dropdownComponents={
<DropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => (
<MenuItem
key={`select-filter-operand-${index}`}
onClick={() => {
handleOperandChange(filterOperand);
}}
text={getOperandLabel(filterOperand)}
/>
))}
<SelectableList
hotkeyScope={dropdownId}
selectableItemIdArray={operandsForFilterType.map(
(operand) => operand,
)}
selectableListInstanceId={dropdownId}
>
{operandsForFilterType.map((filterOperand, index) => (
<SelectableListItem
itemId={filterOperand}
key={`select-filter-operand-${index}`}
onEnter={() => {
handleOperandChange(filterOperand);
}}
>
<MenuItem
focused={selectedItemId === filterOperand}
onClick={() => {
handleOperandChange(filterOperand);
}}
text={getOperandLabel(filterOperand)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: dropdownId }}

View File

@ -15,6 +15,9 @@ import { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
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 { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
@ -72,6 +75,14 @@ export const AdvancedFilterSubFieldSelectMenu = ({
setObjectFilterDropdownIsSelectingCompositeField(false);
};
const { advancedFilterFieldSelectDropdownId } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
advancedFilterFieldSelectDropdownId,
);
if (!isDefined(objectFilterDropdownSubMenuFieldType)) {
return null;
}
@ -86,6 +97,11 @@ export const AdvancedFilterSubFieldSelectMenu = ({
fieldMetadataItemUsedInDropdown.type,
);
const selectableItemIdArray = [
'-1',
...options.map((subFieldName) => subFieldName),
];
return (
<>
<DropdownMenuHeader
@ -99,35 +115,58 @@ export const AdvancedFilterSubFieldSelectMenu = ({
{getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
<SelectableList
hotkeyScope={advancedFilterFieldSelectDropdownId}
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
>
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
focused={selectedItemId === '-1'}
onClick={() => {
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
</SelectableListItem>
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
<SelectableListItem
itemId={subFieldName}
key={`select-filter-${index}`}
onEnter={() => {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
))}
}}
>
<MenuItem
focused={selectedItemId === subFieldName}
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</>
);

View File

@ -94,9 +94,6 @@ export const ObjectFilterDropdownBooleanSelect = () => {
selectableListInstanceId="boolean-select"
selectableItemIdArray={options.map((option) => option.toString())}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
onEnter={(itemId) => {
handleOptionSelect(itemId === 'true');
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{options.map((option) => (

View File

@ -11,14 +11,9 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useSelectFilterUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
@ -96,36 +91,6 @@ export const ObjectFilterDropdownFilterSelect = ({
(fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id),
);
const selectableFieldMetadataItemIds = filterableFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
);
const { selectFilterUsedInDropdown } = useSelectFilterUsedInDropdown();
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
fieldMetadataItemIdUsedInDropdownComponentState,
);
const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID);
const handleEnter = (fieldMetadataItemId: string) => {
const selectedFieldMetadataItem = filterableFieldMetadataItems.find(
(fieldMetadataItem) => fieldMetadataItem.id === fieldMetadataItemId,
);
if (!isDefined(selectedFieldMetadataItem)) {
return;
}
resetSelectedItem();
selectFilterUsedInDropdown({
fieldMetadataItemId,
});
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
};
const shouldShowSeparator =
visibleColumnsFieldMetadataItems.length > 0 &&
hiddenColumnsFieldMetadataItems.length > 0;
@ -137,6 +102,15 @@ export const ObjectFilterDropdownFilterSelect = ({
const { t } = useLingui();
const selectableFieldMetadataItemIds = [
...visibleColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
...hiddenColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
];
return (
<>
<StyledInput
@ -151,34 +125,21 @@ export const ObjectFilterDropdownFilterSelect = ({
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableFieldMetadataItemIds}
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<SelectableItem
itemId={visibleFieldMetadataItem.id}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
fieldMetadataItemToSelect={visibleFieldMetadataItem}
/>
</SelectableItem>
),
)}
{visibleColumnsFieldMetadataItems.map((visibleFieldMetadataItem) => (
<ObjectFilterDropdownFilterSelectMenuItem
key={visibleFieldMetadataItem.id}
fieldMetadataItemToSelect={visibleFieldMetadataItem}
/>
))}
{shouldShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsFieldMetadataItems.map(
(hiddenFieldMetadataItem, index) => (
<SelectableItem
itemId={hiddenFieldMetadataItem.id}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
/>
</SelectableItem>
),
)}
{hiddenColumnsFieldMetadataItems.map((hiddenFieldMetadataItem) => (
<ObjectFilterDropdownFilterSelectMenuItem
key={hiddenFieldMetadataItem.id}
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
/>
))}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}

View File

@ -1,5 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
@ -9,6 +10,7 @@ import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-rec
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
@ -19,6 +21,9 @@ import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/con
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 { 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 { 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';
@ -130,6 +135,10 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
setObjectFilterDropdownFilterIsSelected(false);
setSubFieldNameUsedInDropdown(null);
};
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_FILTER_DROPDOWN_ID,
);
if (!isDefined(objectFilterDropdownSubMenuFieldType)) {
return null;
@ -170,35 +179,65 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
}
/> */}
<DropdownMenuItemsContainer>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
/>
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={['-1', ...options]}
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
>
<SelectableListItem
itemId={'-1'}
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
focused={selectedItemId === '-1'}
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
onClick={() => {
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
LeftIcon={IconApps}
text={`Any ${getFilterableFieldTypeLabel(
objectFilterDropdownSubMenuFieldType,
)} field`}
/>
</SelectableListItem>
{subFieldsAreFilterable &&
options.map((subFieldName, index) => (
<SelectableListItem
itemId={subFieldName}
key={`select-filter-${index}`}
onEnter={() => {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
))}
}}
>
<MenuItem
focused={selectedItemId === subFieldName}
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</>
);

View File

@ -14,6 +14,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
@ -23,7 +24,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
fieldMetadataItemToSelect: FieldMetadataItem;
@ -132,13 +133,17 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
};
return (
<MenuItemSelect
selected={false}
hovered={isSelectedItem}
onClick={handleClick}
LeftIcon={Icon}
text={fieldMetadataItemToSelect.label}
hasSubMenu={shouldShowSubMenu}
/>
<SelectableListItem
itemId={fieldMetadataItemToSelect.id}
onEnter={handleClick}
>
<MenuItem
focused={isSelectedItem}
onClick={handleClick}
LeftIcon={Icon}
text={fieldMetadataItemToSelect.label}
hasSubMenu={shouldShowSubMenu}
/>
</SelectableListItem>
);
};

View File

@ -6,7 +6,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useIcons } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
export type ObjectFilterDropdownFilterSelectMenuItemV2Props = {
fieldMetadataItemToSelect: FieldMetadataItem;
@ -37,9 +37,8 @@ export const ObjectFilterDropdownFilterSelectMenuItemV2 = ({
};
return (
<MenuItemSelect
selected={false}
hovered={isSelectedItem}
<MenuItem
focused={isSelectedItem}
onClick={handleClick}
LeftIcon={Icon}
text={fieldMetadataItemToSelect.label}

View File

@ -167,12 +167,6 @@ export const ObjectFilterDropdownOptionSelect = () => {
selectableListInstanceId={componentInstanceId}
selectableItemIdArray={objectRecordsIds}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
onEnter={(itemId) => {
const option = optionsInDropdown.find((option) => option.id === itemId);
if (isDefined(option)) {
handleMultipleOptionSelectChange(option, !option.isSelected);
}
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (

View File

@ -1,13 +1,18 @@
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 { 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 { 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 { 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 { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@ -74,6 +79,18 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const isDefaultView = currentView?.key === 'INDEX';
const nbsp = '\u00A0';
const selectableItemIdArray = [
ViewType.Table,
...(isDefaultView ? [] : [ViewType.Kanban]),
ViewOpenRecordInType.SIDE_PANEL,
...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []),
];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
return (
<>
<DropdownMenuHeader
@ -86,81 +103,132 @@ export const ObjectOptionsDropdownLayoutContent = () => {
>
{t`Layout`}
</DropdownMenuHeader>
{!!currentView && (
<DropdownMenuItemsContainer>
<MenuItemSelect
LeftIcon={IconTable}
text={t`Table`}
selected={currentView?.type === ViewType.Table}
onClick={async () => {
if (currentView?.type !== ViewType.Table) {
await setAndPersistViewType(ViewType.Table);
}
}}
/>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
text={t`Kanban`}
disabled={isDefaultView}
contextualText={
isDefaultView ? (
<>
{nbsp}·{nbsp}
<OverflowingTextWithTooltip
text={t`Not available for default view`}
/>
</>
) : availableFieldsForKanban.length === 0 ? (
t`Create Select...`
) : undefined
}
selected={currentView?.type === ViewType.Kanban}
onClick={handleSelectKanbanViewType}
/>
<DropdownMenuSeparator />
<MenuItem
onClick={() => onContentChange('layoutOpenIn')}
LeftIcon={
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
? IconLayoutSidebarRight
: IconLayoutNavbar
}
text={t`Open in`}
contextualText={
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
? t`Side Panel`
: t`Record Page`
}
hasSubMenu
/>
{currentView?.type === ViewType.Kanban && (
<>
<MenuItem
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId={ViewType.Table}
onEnter={() => {
setAndPersistViewType(ViewType.Table);
}}
>
<MenuItemSelect
LeftIcon={IconTable}
text={t`Table`}
selected={currentView?.type === ViewType.Table}
focused={selectedItemId === ViewType.Table}
onClick={async () => {
if (currentView?.type !== ViewType.Table) {
await setAndPersistViewType(ViewType.Table);
}
}}
/>
</SelectableListItem>
<SelectableListItem
itemId={ViewType.Kanban}
onEnter={() => {
setAndPersistViewType(ViewType.Kanban);
}}
>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
text={t`Kanban`}
disabled={isDefaultView}
focused={selectedItemId === ViewType.Kanban}
contextualText={
isDefaultView ? (
<>
{nbsp}·{nbsp}
<OverflowingTextWithTooltip
text={t`Not available for default view`}
/>
</>
) : availableFieldsForKanban.length === 0 ? (
t`Create Select...`
) : undefined
}
selected={currentView?.type === ViewType.Kanban}
onClick={handleSelectKanbanViewType}
/>
</SelectableListItem>
<DropdownMenuSeparator />
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}
onEnter={() => {
onContentChange('layoutOpenIn');
}}
>
<MenuItem
focused={selectedItemId === ViewOpenRecordInType.SIDE_PANEL}
LeftIcon={
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
? IconLayoutSidebarRight
: IconLayoutNavbar
}
text={t`Open in`}
contextualText={
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
? t`Side Panel`
: t`Record Page`
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
{currentView?.type === ViewType.Kanban && (
<>
<SelectableListItem
itemId={'Group'}
onEnter={() => {
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields');
}}
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
<MenuItemToggle
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</>
)}
<SelectableListItem
itemId={'Compact view'}
onEnter={() => {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
);
}}
>
<MenuItemToggle
focused={selectedItemId === 'Compact view'}
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
</>
)}
</SelectableList>
</DropdownMenuItemsContainer>
)}
</>

View File

@ -1,9 +1,15 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
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 { 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 { 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { t } from '@lingui/core/macro';
@ -21,6 +27,16 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
const { currentView } = useGetCurrentViewOnly();
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
const selectableItemIdArray = [
ViewOpenRecordInType.SIDE_PANEL,
ViewOpenRecordInType.RECORD_PAGE,
];
return (
<>
<DropdownMenuHeader
@ -34,30 +50,60 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
{t`Open in`}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<MenuItemSelect
LeftIcon={IconLayoutSidebarRight}
text={t`Side Panel`}
selected={recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL}
onClick={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.SIDE_PANEL,
currentView,
)
}
/>
<MenuItemSelect
LeftIcon={IconLayoutNavbar}
text={t`Record Page`}
selected={
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
}
onClick={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.RECORD_PAGE,
currentView,
)
}
/>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}
onEnter={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.SIDE_PANEL,
currentView,
)
}
>
<MenuItemSelect
LeftIcon={IconLayoutSidebarRight}
text={t`Side Panel`}
selected={
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
}
focused={selectedItemId === ViewOpenRecordInType.SIDE_PANEL}
onClick={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.SIDE_PANEL,
currentView,
)
}
/>
</SelectableListItem>
<SelectableListItem
itemId={ViewOpenRecordInType.RECORD_PAGE}
onEnter={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.RECORD_PAGE,
currentView,
)
}
>
<MenuItemSelect
LeftIcon={IconLayoutNavbar}
text={t`Record Page`}
selected={
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
}
onClick={() =>
setAndPersistOpenRecordIn(
ViewOpenRecordInType.RECORD_PAGE,
currentView,
)
}
focused={selectedItemId === ViewOpenRecordInType.RECORD_PAGE}
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer>
</>
);

View File

@ -1,6 +1,7 @@
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';
@ -9,6 +10,9 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
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';
@ -75,92 +79,155 @@ export const ObjectOptionsDropdownMenuContent = () => {
const isDefaultView = currentView?.key === 'INDEX';
const selectableItemIdArray = [
'Layout',
'Fields',
...(isDefaultView ? [] : ['Group']),
'Copy link to view',
...(isDefaultView ? [] : ['Delete view']),
];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
return (
<>
{currentView && (
<ObjectOptionsDropdownMenuViewName currentView={currentView} />
)}
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem
onClick={() => onContentChange('layout')}
LeftIcon={viewTypeIconMapping(currentView?.type ?? ViewType.Table)}
text={t`Layout`}
contextualText={`${capitalize(currentView?.type ?? '')}`}
hasSubMenu
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem
onClick={() => onContentChange('fields')}
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
<div id="group-by-menu-item">
<MenuItem
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={
isDefaultView
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</div>
{!isGroupByEnabled && (
<AppTooltip
anchorSelect={`#group-by-menu-item`}
content={t`Not available on Default View`}
noArrow
place="bottom"
width="100%"
/>
)}
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId="Layout"
onEnter={() => onContentChange('layout')}
>
<MenuItem
focused={selectedItemId === 'Layout'}
onClick={() => onContentChange('layout')}
LeftIcon={viewTypeIconMapping(
currentView?.type ?? ViewType.Table,
)}
text={t`Layout`}
contextualText={`${capitalize(currentView?.type ?? '')}`}
hasSubMenu
/>
</SelectableListItem>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<MenuItem
onClick={() => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl);
enqueueSnackBar('Link copied to clipboard', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
}}
LeftIcon={IconCopy}
text={t`Copy link to view`}
/>
<div id="delete-view-menu-item">
<MenuItem
onClick={() => handleDelete()}
LeftIcon={IconTrash}
text={t`Delete view`}
disabled={currentView?.key === 'INDEX'}
/>
</div>
{currentView?.key === 'INDEX' && (
<AppTooltip
anchorSelect={`#delete-view-menu-item`}
content={t`Not available on Default View`}
noArrow
place="bottom"
width="100%"
/>
)}
</DropdownMenuItemsContainer>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId="Fields"
onEnter={() => onContentChange('fields')}
>
<MenuItem
focused={selectedItemId === 'Fields'}
onClick={() => onContentChange('fields')}
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
</SelectableListItem>
<div id="group-by-menu-item">
<SelectableListItem
itemId="Group"
onEnter={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={
isDefaultView
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
{!isGroupByEnabled && (
<AppTooltip
anchorSelect={`#group-by-menu-item`}
content={t`Not available on Default View`}
noArrow
place="bottom"
width="100%"
/>
)}
<DropdownMenuSeparator />
<SelectableListItem
itemId="Copy link to view"
onEnter={() => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl);
enqueueSnackBar('Link copied to clipboard', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
}}
>
<MenuItem
focused={selectedItemId === 'Copy link to view'}
onClick={() => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl);
enqueueSnackBar('Link copied to clipboard', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
}}
LeftIcon={IconCopy}
text={t`Copy link to view`}
/>
</SelectableListItem>
<div id="delete-view-menu-item">
<SelectableListItem
itemId="Delete view"
onEnter={() => handleDelete()}
>
<MenuItem
focused={selectedItemId === 'Delete view'}
onClick={() => handleDelete()}
LeftIcon={IconTrash}
text={t`Delete view`}
disabled={currentView?.key === 'INDEX'}
/>
</SelectableListItem>
</div>
{currentView?.key === 'INDEX' && (
<AppTooltip
anchorSelect={`#delete-view-menu-item`}
content={t`Not available on Default View`}
noArrow
place="bottom"
width="100%"
/>
)}
</DropdownMenuItemsContainer>
</SelectableList>
</>
);
};

View File

@ -1,14 +1,19 @@
import { useEffect } from 'react';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
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 { 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 { 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 { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import {
IconChevronLeft,
IconHandMove,
@ -32,6 +37,11 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
setRecordGroupSort(sort);
};
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
useEffect(() => {
if (
currentContentId === 'hiddenRecordGroups' &&
@ -41,6 +51,12 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
}
}, [hiddenRecordGroupIds, currentContentId, onContentChange]);
const selectableItemIdArray = [
RecordGroupSort.Manual,
RecordGroupSort.Alphabetical,
RecordGroupSort.ReverseAlphabetical,
];
return (
<>
<DropdownMenuHeader
@ -54,28 +70,58 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
Sort
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<MenuItemSelect
onClick={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
LeftIcon={IconHandMove}
text={RecordGroupSort.Manual}
selected={recordGroupSort === RecordGroupSort.Manual}
/>
<MenuItemSelect
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
}
LeftIcon={IconSortAZ}
text={RecordGroupSort.Alphabetical}
selected={recordGroupSort === RecordGroupSort.Alphabetical}
/>
<MenuItemSelect
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
}
LeftIcon={IconSortZA}
text={RecordGroupSort.ReverseAlphabetical}
selected={recordGroupSort === RecordGroupSort.ReverseAlphabetical}
/>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId={RecordGroupSort.Manual}
onEnter={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
>
<MenuItemSelect
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.Manual)
}
LeftIcon={IconHandMove}
text={RecordGroupSort.Manual}
selected={recordGroupSort === RecordGroupSort.Manual}
focused={selectedItemId === RecordGroupSort.Manual}
/>
</SelectableListItem>
<SelectableListItem
itemId={RecordGroupSort.Alphabetical}
onEnter={() =>
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
}
>
<MenuItemSelect
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
}
LeftIcon={IconSortAZ}
text={RecordGroupSort.Alphabetical}
selected={recordGroupSort === RecordGroupSort.Alphabetical}
focused={selectedItemId === RecordGroupSort.Alphabetical}
/>
</SelectableListItem>
<SelectableListItem
itemId={RecordGroupSort.ReverseAlphabetical}
onEnter={() =>
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
}
>
<MenuItemSelect
onClick={() =>
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
}
LeftIcon={IconSortZA}
text={RecordGroupSort.ReverseAlphabetical}
selected={recordGroupSort === RecordGroupSort.ReverseAlphabetical}
focused={selectedItemId === RecordGroupSort.ReverseAlphabetical}
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer>
</>
);

View File

@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal';
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
@ -10,10 +11,14 @@ 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 { 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 { 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 { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
@ -89,6 +94,17 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
}
}, [hiddenRecordGroupIds, currentContentId, onContentChange]);
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
const selectableItemIdArray = [
...(currentView?.key !== 'INDEX' ? ['GroupBy', 'Sort'] : []),
'HideEmptyGroups',
...(hiddenRecordGroupIds.length > 0 ? ['HiddenGroups'] : []),
];
return (
<>
<DropdownMenuHeader
@ -102,31 +118,55 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
Group
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{currentView?.key !== 'INDEX' && (
<>
<MenuItem
onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList}
text={t`Group by`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selectableItemIdArray={selectableItemIdArray}
>
{currentView?.key !== 'INDEX' && (
<>
<SelectableListItem
itemId="GroupBy"
onEnter={() => onContentChange('recordGroupFields')}
>
<MenuItem
focused={selectedItemId === 'GroupBy'}
onClick={() => onContentChange('recordGroupFields')}
LeftIcon={IconLayoutList}
text={t`Group by`}
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
<SelectableListItem
itemId="Sort"
onEnter={() => onContentChange('recordGroupSort')}
>
<MenuItem
focused={selectedItemId === 'Sort'}
onClick={() => onContentChange('recordGroupSort')}
LeftIcon={IconSortDescending}
text={t`Sort`}
contextualText={recordGroupSort}
hasSubMenu
/>
</SelectableListItem>
</>
)}
<SelectableListItem
itemId="HideEmptyGroups"
onEnter={() => handleHideEmptyRecordGroupChange()}
>
<MenuItemToggle
focused={selectedItemId === 'HideEmptyGroups'}
LeftIcon={IconCircleOff}
onToggleChange={handleHideEmptyRecordGroupChange}
toggled={hideEmptyRecordGroup}
text={t`Hide empty groups`}
toggleSize="small"
/>
<MenuItem
onClick={() => onContentChange('recordGroupSort')}
LeftIcon={IconSortDescending}
text={t`Sort`}
contextualText={recordGroupSort}
hasSubMenu
/>
</>
)}
<MenuItemToggle
LeftIcon={IconCircleOff}
onToggleChange={handleHideEmptyRecordGroupChange}
toggled={hideEmptyRecordGroup}
text={t`Hide empty groups`}
toggleSize="small"
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer>
{visibleRecordGroupIds.length > 0 && (
<>
@ -145,11 +185,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<MenuItemNavigate
onClick={() => onContentChange('hiddenRecordGroups')}
LeftIcon={IconEyeOff}
text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`}
/>
<SelectableListItem
itemId="HiddenGroups"
onEnter={() => onContentChange('hiddenRecordGroups')}
>
<MenuItemNavigate
onClick={() => onContentChange('hiddenRecordGroups')}
LeftIcon={IconEyeOff}
text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`}
/>
</SelectableListItem>
</DropdownMenuItemsContainer>
</>
)}

View File

@ -24,16 +24,19 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
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 { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
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 { useTheme } from '@emotion/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useTheme } from '@emotion/react';
import { IconChevronDown, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { v4 } from 'uuid';
export const StyledInput = styled.input`
background: transparent;
@ -191,6 +194,21 @@ export const ObjectSortDropdownButton = ({
const theme = useTheme();
const selectableItemIdArray = [
...visibleFieldMetadataItems.map((item) => item.id),
...hiddenFieldMetadataItems.map((item) => item.id),
];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_SORT_DROPDOWN_ID,
);
const setSelectedItemId = useSetRecoilComponentStateV2(
selectedItemIdComponentState,
OBJECT_SORT_DROPDOWN_ID,
);
return (
<Dropdown
dropdownId={OBJECT_SORT_DROPDOWN_ID}
@ -198,20 +216,28 @@ export const ObjectSortDropdownButton = ({
dropdownOffset={{ y: 8 }}
clickableComponent={
<StyledHeaderDropdownButton
onClick={handleButtonClick}
onClick={() => {
handleButtonClick();
setSelectedItemId(selectableItemIdArray[0]);
}}
isUnfolded={isDropdownOpen}
>
<Trans>Sort</Trans>
</StyledHeaderDropdownButton>
}
dropdownComponents={
<>
<SelectableList
selectableListInstanceId={OBJECT_SORT_DROPDOWN_ID}
hotkeyScope={hotkeyScope.scope}
selectableItemIdArray={selectableItemIdArray}
>
{isRecordSortDirectionMenuUnfolded && (
<StyledSelectedSortDirectionContainer>
<DropdownMenuItemsContainer>
{RECORD_SORT_DIRECTIONS.map((sortDirection, index) => (
<MenuItem
key={index}
focused={selectedItemId === sortDirection}
onClick={() => handleSortDirectionClick(sortDirection)}
text={
sortDirection === 'asc' ? t`Ascending` : t`Descending`
@ -244,27 +270,39 @@ export const ObjectSortDropdownButton = ({
<DropdownMenuItemsContainer scrollable={false}>
{visibleFieldMetadataItems.map(
(visibleFieldMetadataItem, index) => (
<MenuItem
testId={`visible-select-sort-${index}`}
key={index}
onClick={() => handleAddSort(visibleFieldMetadataItem)}
LeftIcon={getIcon(visibleFieldMetadataItem.icon)}
text={visibleFieldMetadataItem.label}
/>
<SelectableListItem
key={visibleFieldMetadataItem.id}
itemId={visibleFieldMetadataItem.id}
onEnter={() => handleAddSort(visibleFieldMetadataItem)}
>
<MenuItem
focused={selectedItemId === visibleFieldMetadataItem.id}
testId={`visible-select-sort-${index}`}
onClick={() => handleAddSort(visibleFieldMetadataItem)}
LeftIcon={getIcon(visibleFieldMetadataItem.icon)}
text={visibleFieldMetadataItem.label}
/>
</SelectableListItem>
),
)}
{shouldShowSeparator && <DropdownMenuSeparator />}
{hiddenFieldMetadataItems.map((hiddenFieldMetadataItem, index) => (
<MenuItem
testId={`hidden-select-sort-${index}`}
key={index}
onClick={() => handleAddSort(hiddenFieldMetadataItem)}
LeftIcon={getIcon(hiddenFieldMetadataItem.icon)}
text={hiddenFieldMetadataItem.label}
/>
<SelectableListItem
key={hiddenFieldMetadataItem.id}
itemId={hiddenFieldMetadataItem.id}
onEnter={() => handleAddSort(hiddenFieldMetadataItem)}
>
<MenuItem
focused={selectedItemId === hiddenFieldMetadataItem.id}
testId={`hidden-select-sort-${index}`}
onClick={() => handleAddSort(hiddenFieldMetadataItem)}
LeftIcon={getIcon(hiddenFieldMetadataItem.icon)}
text={hiddenFieldMetadataItem.label}
/>
</SelectableListItem>
))}
</DropdownMenuItemsContainer>
</>
</SelectableList>
}
onClose={handleDropdownButtonClose}
/>

View File

@ -4,9 +4,8 @@ import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/get
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
export const useRecordBoardSelection = (recordBoardId: string) => {
@ -22,17 +21,16 @@ export const useRecordBoardSelection = (recordBoardId: string) => {
recordBoardId,
);
const isActionMenuDropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
getActionMenuDropdownIdFromActionMenuId(
getActionMenuIdFromRecordIndexId(recordBoardId),
),
const { closeDropdown } = useDropdownV2();
const dropdownId = getActionMenuDropdownIdFromActionMenuId(
getActionMenuIdFromRecordIndexId(recordBoardId),
);
const resetRecordSelection = useRecoilCallback(
({ snapshot, set }) =>
() => {
set(isActionMenuDropdownOpenState, false);
closeDropdown(dropdownId);
const recordIds = getSnapshotValue(
snapshot,
@ -44,7 +42,8 @@ export const useRecordBoardSelection = (recordBoardId: string) => {
}
},
[
isActionMenuDropdownOpenState,
closeDropdown,
dropdownId,
recordBoardSelectedRecordIdsSelector,
isRecordBoardCardSelectedFamilyState,
],
@ -67,17 +66,17 @@ export const useRecordBoardSelection = (recordBoardId: string) => {
);
const checkIfLastUnselectAndCloseDropdown = useRecoilCallback(
({ snapshot, set }) =>
({ snapshot }) =>
() => {
const recordIds = getSnapshotValue(
snapshot,
recordBoardSelectedRecordIdsSelector,
);
if (recordIds.length === 0) {
set(isActionMenuDropdownOpenState, false);
closeDropdown(dropdownId);
}
},
[recordBoardSelectedRecordIdsSelector, isActionMenuDropdownOpenState],
[recordBoardSelectedRecordIdsSelector, closeDropdown, dropdownId],
);
return {

View File

@ -7,6 +7,7 @@ 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 { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
@ -121,7 +122,9 @@ export const RecordBoardCard = () => {
x: event.clientX,
y: event.clientY,
});
openDropdown(actionMenuDropdownId);
openDropdown(actionMenuDropdownId, {
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
});
};
const handleCardClick = () => {

View File

@ -15,7 +15,7 @@ import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -28,7 +28,7 @@ import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
export const StyledSelectableItem = styled(SelectableItem)`
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;

View File

@ -3,10 +3,10 @@ import styled from '@emotion/styled';
import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId';
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableItem)`
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;

View File

@ -6,7 +6,7 @@ import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/re
import { multipleRecordPickerIsSelectedComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
@ -14,7 +14,7 @@ import { Avatar } from 'twenty-ui/display';
import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { SearchRecord } from '~/generated-metadata/graphql';
export const StyledSelectableItem = styled(SelectableItem)`
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
@ -62,6 +62,7 @@ export const MultipleRecordPickerMenuItemContent = ({
<StyledSelectableItem
itemId={searchRecord.recordId}
key={searchRecord.recordId}
onEnter={() => handleSelectChange(!isRecordSelectedWithObjectItem)}
>
<MenuItemMultiSelectAvatar
onSelectChange={(isSelected) => handleSelectChange(isSelected)}

View File

@ -4,21 +4,19 @@ 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 { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableItem)`
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
@ -45,11 +43,6 @@ export const MultipleRecordPickerMenuItems = ({
const { resetSelectedItem } = useSelectableList(
selectableListComponentInstanceId,
);
const singlePickableMorphItemFamilySelector =
useRecoilComponentCallbackStateV2(
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
componentInstanceId,
);
const multipleRecordPickerPickableMorphItemsState =
useRecoilComponentCallbackStateV2(
@ -82,42 +75,12 @@ export const MultipleRecordPickerMenuItems = ({
[multipleRecordPickerPickableMorphItemsState],
);
const handleEnter = useRecoilCallback(
({ snapshot }) => {
return (selectedId: string) => {
const pickableMorphItem = snapshot
.getLoadable(singlePickableMorphItemFamilySelector(selectedId))
.getValue();
if (!isDefined(pickableMorphItem)) {
return;
}
const selectedMorphItem = {
...pickableMorphItem,
isSelected: !pickableMorphItem.isSelected,
};
handleChange(selectedMorphItem);
onChange?.(selectedMorphItem);
resetSelectedItem();
};
},
[
handleChange,
onChange,
resetSelectedItem,
singlePickableMorphItemFamilySelector,
],
);
return (
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={pickableRecordIds}
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
onEnter={handleEnter}
>
{pickableRecordIds.map((recordId) => {
return (

View File

@ -1,19 +1,11 @@
import styled from '@emotion/styled';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useCallback } from 'react';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;
export const MultipleRecordPickerSearchInput = () => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext,

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
@ -16,7 +16,7 @@ type SingleRecordPickerMenuItemProps = {
selectedRecord?: SingleRecordPickerRecord;
};
const StyledSelectableItem = styled(SelectableItem)`
const StyledSelectableItem = styled(SelectableListItem)`
width: 100%;
`;
@ -40,14 +40,19 @@ export const SingleRecordPickerMenuItem = ({
);
return (
<StyledSelectableItem itemId={record.id} key={record.id}>
<StyledSelectableItem
itemId={record.id}
key={record.id}
onEnter={() => {
onRecordSelected(record);
}}
>
<MenuItemSelectAvatar
key={record.id}
testId="menu-item"
onClick={() => onRecordSelected(record)}
text={record.name}
selected={selectedRecord?.id === record.id}
hovered={isSelectedItemId}
focused={isSelectedItemId}
avatar={
<Avatar
avatarUrl={record.avatarUrl}

View File

@ -14,6 +14,7 @@ import { singleRecordPickerSelectedIdComponentState } from '@/object-record/reco
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 { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
@ -108,14 +109,6 @@ export const SingleRecordPickerMenuItems = ({
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const recordIndex = recordsInDropdown.findIndex(
(record) => record.id === itemId,
);
setSelectedRecordId(itemId);
onRecordSelected(recordsInDropdown[recordIndex]);
resetSelectedItem();
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{loading && !isFiltered ? (
@ -128,17 +121,25 @@ export const SingleRecordPickerMenuItems = ({
case 'select-none': {
return (
emptyLabel && (
<MenuItemSelect
<SelectableListItem
key={record.id}
onClick={() => {
itemId={record.id}
onEnter={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
LeftIcon={EmptyIcon}
text={emptyLabel}
selected={isUndefined(selectedRecordId)}
hovered={isSelectedSelectNoneButton}
/>
>
<MenuItemSelect
onClick={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
LeftIcon={EmptyIcon}
text={emptyLabel}
selected={isUndefined(selectedRecordId)}
focused={isSelectedSelectNoneButton}
/>
</SelectableListItem>
)
);
}

View File

@ -2,15 +2,15 @@ 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';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useTriggerActionMenuDropdown = ({
recordTableId,
}: {
@ -25,18 +25,17 @@ export const useTriggerActionMenuDropdown = ({
recordTableId,
);
const actionMenuDropdownId =
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId);
const recordIndexActionMenuDropdownPositionState = extractComponentState(
recordIndexActionMenuDropdownPositionComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId),
actionMenuDropdownId,
);
const isActionMenuDropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId),
);
const { openDropdown } = useDropdown(actionMenuDropdownId);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const { closeCommandMenu } = useCommandMenu();
const triggerActionMenuDropdown = useRecoilCallback(
({ set, snapshot }) =>
@ -57,19 +56,17 @@ export const useTriggerActionMenuDropdown = ({
set(isRowSelectedFamilyState(recordId), true);
}
set(isActionMenuDropdownOpenState, true);
closeCommandMenu();
const actionMenuDropdownId =
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId);
setActiveDropdownFocusIdAndMemorizePrevious(actionMenuDropdownId);
openDropdown({
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
});
},
[
isActionMenuDropdownOpenState,
isRowSelectedFamilyState,
recordIndexActionMenuDropdownPositionState,
setActiveDropdownFocusIdAndMemorizePrevious,
actionMenuInstanceId,
isRowSelectedFamilyState,
closeCommandMenu,
openDropdown,
],
);

View File

@ -7,6 +7,7 @@ import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
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';
@ -93,43 +94,38 @@ export const MultipleSelectDropdown = ({
selectableListInstanceId={selectableListId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const item = itemsInDropdown.findIndex(
(entity) => entity.id === itemId,
);
const itemIsSelectedInDropwdown = filteredSelectedItems.find(
(entity) => entity.id === itemId,
);
handleItemSelectChange(
itemsInDropdown[item],
!itemIsSelectedInDropwdown,
);
resetSelectedItem();
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{itemsInDropdown?.map((item) => {
return (
<MenuItemMultiSelectAvatar
key={item.id}
selected={item.isSelected}
isKeySelected={item.id === selectedItemId}
onSelectChange={(newCheckedValue) => {
<SelectableListItem
itemId={item.id}
onEnter={() => {
resetSelectedItem();
handleItemSelectChange(item, newCheckedValue);
handleItemSelectChange(item, !item.isSelected);
}}
avatar={
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={item.name}
avatarUrl={item.avatarUrl}
LeftIcon={item.AvatarIcon}
avatarType={item.avatarType}
isIconInverted={item.isIconInverted}
placeholderColorSeed={item.id}
/>
}
/>
>
<MenuItemMultiSelectAvatar
key={item.id}
selected={item.isSelected}
isKeySelected={item.id === selectedItemId}
onSelectChange={(newCheckedValue) => {
resetSelectedItem();
handleItemSelectChange(item, newCheckedValue);
}}
avatar={
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={item.name}
avatarUrl={item.avatarUrl}
LeftIcon={item.AvatarIcon}
avatarType={item.avatarType}
isIconInverted={item.isIconInverted}
placeholderColorSeed={item.id}
/>
}
/>
</SelectableListItem>
);
})}
{showNoResult && <MenuItem text="No results" />}