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,5 +1,10 @@
import { ActionDisplayProps } from '@/action-menu/actions/display/components/ActionDisplay'; import { ActionDisplayProps } from '@/action-menu/actions/display/components/ActionDisplay';
import { getActionLabel } from '@/action-menu/utils/getActionLabel'; import { getActionLabel } from '@/action-menu/utils/getActionLabel';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
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';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
@ -22,12 +27,25 @@ export const ActionDropdownItem = ({
} }
}; };
const selectableListInstanceId = useAvailableComponentInstanceIdOrThrow(
SelectableListComponentInstanceContext,
);
const isSelected = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector,
action.key,
selectableListInstanceId,
);
return ( return (
<MenuItem <SelectableListItem itemId={action.key} onEnter={handleClick}>
key={action.key} <MenuItem
LeftIcon={action.Icon} focused={isSelected}
onClick={handleClick} key={action.key}
text={getActionLabel(action.label)} LeftIcon={action.Icon}
/> onClick={handleClick}
text={getActionLabel(action.label)}
/>
</SelectableListItem>
); );
}; };

View File

@ -1,9 +1,7 @@
import { ActionDisplayProps } from '@/action-menu/actions/display/components/ActionDisplay'; import { ActionDisplayProps } from '@/action-menu/actions/display/components/ActionDisplay';
import { getActionLabel } from '@/action-menu/utils/getActionLabel'; import { getActionLabel } from '@/action-menu/utils/getActionLabel';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableListListenToEnterHotkeyOnItem } from '@/ui/layout/selectable-list/hooks/useSelectableListListenToEnterHotkeyOnItem';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -25,14 +23,8 @@ export const ActionListItem = ({
} }
}; };
useSelectableListListenToEnterHotkeyOnItem({
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
itemId: action.key,
onEnter: handleClick,
});
return ( return (
<SelectableItem itemId={action.key}> <SelectableListItem itemId={action.key} onEnter={handleClick}>
<CommandMenuItem <CommandMenuItem
id={action.key} id={action.key}
Icon={action.Icon} Icon={action.Icon}
@ -42,6 +34,6 @@ export const ActionListItem = ({
onClick={onClick} onClick={onClick}
hotKeys={action.hotKeys} hotKeys={action.hotKeys}
/> />
</SelectableItem> </SelectableListItem>
); );
}; };

View File

@ -114,6 +114,13 @@ export const AsDropdownItem: Story = {
onClick: addToFavoritesMock, onClick: addToFavoritesMock,
}, },
decorators: [ decorators: [
(Story) => (
<SelectableListComponentInstanceContext.Provider
value={{ instanceId: 'story' }}
>
<Story />
</SelectableListComponentInstanceContext.Provider>
),
(Story) => ( (Story) => (
<ActionMenuContext.Provider <ActionMenuContext.Provider
value={{ value={{

View File

@ -2,6 +2,7 @@ import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-action
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { createMockActionMenuActions } from '@/action-menu/mock/action-menu-actions.mock'; import { createMockActionMenuActions } from '@/action-menu/mock/action-menu-actions.mock';
import { getActionLabel } from '@/action-menu/utils/getActionLabel'; import { getActionLabel } from '@/action-menu/utils/getActionLabel';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { fn, userEvent, within } from '@storybook/test'; import { fn, userEvent, within } from '@storybook/test';
@ -11,7 +12,17 @@ import { ActionDropdownItem } from '../ActionDropdownItem';
const meta: Meta<typeof ActionDropdownItem> = { const meta: Meta<typeof ActionDropdownItem> = {
title: 'Modules/ActionMenu/Actions/Display/ActionDropdownItem', title: 'Modules/ActionMenu/Actions/Display/ActionDropdownItem',
component: ActionDropdownItem, component: ActionDropdownItem,
decorators: [ComponentDecorator, RouterDecorator], decorators: [
(Story) => (
<SelectableListComponentInstanceContext.Provider
value={{ instanceId: 'story' }}
>
<Story />
</SelectableListComponentInstanceContext.Provider>
),
ComponentDecorator,
RouterDecorator,
],
}; };
export default meta; export default meta;

View File

@ -7,6 +7,8 @@ import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-men
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@ -41,6 +43,16 @@ export const CommandMenuActionMenuDropdown = () => {
[toggleDropdown], [toggleDropdown],
); );
const recordSelectionActions = actions.filter(
(action) => action.scope === ActionScope.RecordSelection,
);
const selectableItemIdArray = recordSelectionActions.map(
(action) => action.key,
);
const { setSelectedItemId } = useSelectableList(actionMenuId);
return ( return (
<Dropdown <Dropdown
dropdownId={getRightDrawerActionMenuDropdownIdFromActionMenuId( dropdownId={getRightDrawerActionMenuDropdownIdFromActionMenuId(
@ -56,13 +68,22 @@ export const CommandMenuActionMenuDropdown = () => {
} }
dropdownPlacement="top-end" dropdownPlacement="top-end"
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }} dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
onOpen={() => {
setSelectedItemId(selectableItemIdArray[0]);
}}
dropdownComponents={ dropdownComponents={
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{actions <SelectableList
.filter((action) => action.scope === ActionScope.RecordSelection) selectableListInstanceId={actionMenuId}
.map((action) => ( hotkeyScope={
CommandMenuActionMenuDropdownHotkeyScope.CommandMenuActionMenuDropdown
}
selectableItemIdArray={selectableItemIdArray}
>
{recordSelectionActions.map((action) => (
<ActionComponent action={action} key={action.key} /> <ActionComponent action={action} key={action.key} />
))} ))}
</SelectableList>
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
} }
/> />

View File

@ -9,8 +9,12 @@ import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/get
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useContext } from 'react'; import { useContext } from 'react';
@ -42,7 +46,7 @@ export const RecordIndexActionMenuDropdown = () => {
); );
const dropdownId = getActionMenuDropdownIdFromActionMenuId(actionMenuId); const dropdownId = getActionMenuDropdownIdFromActionMenuId(actionMenuId);
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdownV2();
const actionMenuDropdownPosition = useRecoilValue( const actionMenuDropdownPosition = useRecoilValue(
extractComponentState( extractComponentState(
@ -53,6 +57,16 @@ export const RecordIndexActionMenuDropdown = () => {
const { openCommandMenu } = useCommandMenu(); const { openCommandMenu } = useCommandMenu();
const selectedItemIdArray = [
...recordIndexActions.map((action) => action.key),
'more-actions',
];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
dropdownId,
);
return ( return (
<Dropdown <Dropdown
dropdownId={dropdownId} dropdownId={dropdownId}
@ -69,18 +83,33 @@ export const RecordIndexActionMenuDropdown = () => {
dropdownComponents={ dropdownComponents={
<StyledDropdownMenuContainer className="action-menu-dropdown"> <StyledDropdownMenuContainer className="action-menu-dropdown">
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{recordIndexActions.map((action) => ( <SelectableList
<ActionComponent action={action} key={action.key} /> hotkeyScope={ActionMenuDropdownHotkeyScope.ActionMenuDropdown}
))} selectableItemIdArray={selectedItemIdArray}
<MenuItem selectableListInstanceId={dropdownId}
key="more-actions" >
LeftIcon={IconLayoutSidebarRightExpand} {recordIndexActions.map((action) => (
onClick={() => { <ActionComponent action={action} key={action.key} />
closeDropdown(); ))}
openCommandMenu(); <SelectableListItem
}} itemId="more-actions"
text="More actions" key="more-actions"
/> onEnter={() => {
closeDropdown(dropdownId);
openCommandMenu();
}}
>
<MenuItem
LeftIcon={IconLayoutSidebarRightExpand}
onClick={() => {
closeDropdown(dropdownId);
openCommandMenu();
}}
focused={selectedItemId === 'more-actions'}
text="More actions"
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</StyledDropdownMenuContainer> </StyledDropdownMenuContainer>
} }

View File

@ -7,7 +7,6 @@ import { useMatchingCommandMenuActions } from '@/command-menu/hooks/useMatchingC
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -89,9 +88,7 @@ export const CommandMenu = () => {
> >
{isDefined(previousContextStoreCurrentObjectMetadataItemId) && ( {isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
<CommandGroup heading={t`Context`}> <CommandGroup heading={t`Context`}>
<SelectableItem itemId={RESET_CONTEXT_TO_SELECTION}> <ResetContextToSelectionCommandButton />
<ResetContextToSelectionCommandButton />
</SelectableItem>
</CommandGroup> </CommandGroup>
)} )}
</CommandMenuList> </CommandMenuList>

View File

@ -4,8 +4,6 @@ import { ActionGroupConfig } from '@/command-menu/components/CommandMenu';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
@ -64,9 +62,6 @@ export const CommandMenuList = ({
loading = false, loading = false,
noResults = false, noResults = false,
}: CommandMenuListProps) => { }: CommandMenuListProps) => {
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
const setHasUserSelectedCommand = useSetRecoilState( const setHasUserSelectedCommand = useSetRecoilState(
hasUserSelectedCommandState, hasUserSelectedCommandState,
); );
@ -82,12 +77,6 @@ export const CommandMenuList = ({
selectableListInstanceId="command-menu-list" selectableListInstanceId="command-menu-list"
hotkeyScope={AppHotkeyScope.CommandMenuOpen} hotkeyScope={AppHotkeyScope.CommandMenuOpen}
selectableItemIdArray={selectableItemIds} selectableItemIdArray={selectableItemIds}
onEnter={(itemId) => {
if (itemId === RESET_CONTEXT_TO_SELECTION) {
resetPreviousCommandMenuContext();
return;
}
}}
onSelect={() => { onSelect={() => {
setHasUserSelectedCommand(true); setHasUserSelectedCommand(true);
}} }}

View File

@ -6,6 +6,7 @@ import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useRese
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -42,17 +43,22 @@ export const ResetContextToSelectionCommandButton = () => {
} }
return ( return (
<CommandMenuItem <SelectableListItem
id={RESET_CONTEXT_TO_SELECTION} itemId={RESET_CONTEXT_TO_SELECTION}
Icon={IconArrowBackUp} onEnter={resetPreviousCommandMenuContext}
label={t`Reset to`} >
RightComponent={ <CommandMenuItem
<CommandMenuContextRecordsChip id={RESET_CONTEXT_TO_SELECTION}
objectMetadataItemId={objectMetadataItem.id} Icon={IconArrowBackUp}
instanceId={COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID} label={t`Reset to`}
/> RightComponent={
} <CommandMenuContextRecordsChip
onClick={resetPreviousCommandMenuContext} objectMetadataItemId={objectMetadataItem.id}
/> instanceId={COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID}
/>
}
onClick={resetPreviousCommandMenuContext}
/>
</SelectableListItem>
); );
}; };

View File

@ -8,6 +8,18 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
const mockGoBackToPreviousHotkeyScope = jest.fn();
const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn();
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({
usePreviousHotkeyScope: () => ({
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope:
mockSetHotkeyScopeAndMemorizePreviousScope,
}),
}));
const Wrapper = ({ children }: { children: React.ReactNode }) => ( const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot> <RecoilRoot>
@ -47,6 +59,10 @@ const renderHooks = () => {
}; };
describe('useCommandMenu', () => { describe('useCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should open and close the command menu', () => { it('should open and close the command menu', () => {
const { result } = renderHooks(); const { result } = renderHooks();
@ -55,6 +71,12 @@ describe('useCommandMenu', () => {
}); });
expect(result.current.isCommandMenuOpened).toBe(true); expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
CommandMenuHotkeyScope.CommandMenuFocused,
{
commandMenuOpen: true,
},
);
act(() => { act(() => {
result.current.commandMenu.closeCommandMenu(); result.current.commandMenu.closeCommandMenu();
@ -73,6 +95,12 @@ describe('useCommandMenu', () => {
}); });
expect(result.current.isCommandMenuOpened).toBe(true); expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
CommandMenuHotkeyScope.CommandMenuFocused,
{
commandMenuOpen: true,
},
);
act(() => { act(() => {
result.current.commandMenu.toggleCommandMenu(); result.current.commandMenu.toggleCommandMenu();
@ -80,4 +108,21 @@ describe('useCommandMenu', () => {
expect(result.current.isCommandMenuOpened).toBe(false); expect(result.current.isCommandMenuOpened).toBe(false);
}); });
it('should call goBackToPreviousHotkeyScope when closing the command menu', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.openCommandMenu();
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(mockGoBackToPreviousHotkeyScope).not.toHaveBeenCalled();
act(() => {
result.current.commandMenu.closeCommandMenu();
});
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -23,7 +23,6 @@ import { IconList } from 'twenty-ui/display';
const mockCloseDropdown = jest.fn(); const mockCloseDropdown = jest.fn();
const mockResetContextStoreStates = jest.fn(); const mockResetContextStoreStates = jest.fn();
const mockResetSelectedItem = jest.fn(); const mockResetSelectedItem = jest.fn();
const mockGoBackToPreviousHotkeyScope = jest.fn();
const mockEmitRightDrawerCloseEvent = jest.fn(); const mockEmitRightDrawerCloseEvent = jest.fn();
jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({ jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
@ -44,12 +43,6 @@ jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({
}), }),
})); }));
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({
usePreviousHotkeyScope: () => ({
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
}),
}));
jest.mock('@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent', () => ({ jest.mock('@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent', () => ({
emitRightDrawerCloseEvent: () => { emitRightDrawerCloseEvent: () => {
mockEmitRightDrawerCloseEvent(); mockEmitRightDrawerCloseEvent();
@ -231,7 +224,6 @@ describe('useCommandMenuCloseAnimationCompleteCleanup', () => {
expect(mockCloseDropdown).toHaveBeenCalledTimes(1); expect(mockCloseDropdown).toHaveBeenCalledTimes(1);
expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2); expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2);
expect(mockResetSelectedItem).toHaveBeenCalledTimes(1); expect(mockResetSelectedItem).toHaveBeenCalledTimes(1);
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalledTimes(1);
expect(mockEmitRightDrawerCloseEvent).toHaveBeenCalledTimes(1); expect(mockEmitRightDrawerCloseEvent).toHaveBeenCalledTimes(1);
expect(mockCloseDropdown).toHaveBeenCalledWith( expect(mockCloseDropdown).toHaveBeenCalledWith(

View File

@ -2,11 +2,13 @@ import { useRecoilCallback } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown'; import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState'; import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { IconDotsVertical } from 'twenty-ui/display'; import { IconDotsVertical } from 'twenty-ui/display';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
@ -14,16 +16,26 @@ import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
export const useCommandMenu = () => { export const useCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu(); const { navigateCommandMenu } = useNavigateCommandMenu();
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown(); const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(
COMMAND_MENU_COMPONENT_INSTANCE_ID,
);
const closeCommandMenu = useRecoilCallback( const closeCommandMenu = useRecoilCallback(
({ set }) => ({ set, snapshot }) =>
() => { () => {
set(isCommandMenuOpenedState, false); const isCommandMenuOpened = snapshot
set(isCommandMenuClosingState, true); .getLoadable(isCommandMenuOpenedState)
set(isDragSelectionStartEnabledState, true); .getValue();
closeAnyOpenDropdown();
if (isCommandMenuOpened) {
set(isCommandMenuOpenedState, false);
set(isCommandMenuClosingState, true);
set(isDragSelectionStartEnabledState, true);
closeAnyOpenDropdown();
goBackToPreviousHotkeyScope();
}
}, },
[closeAnyOpenDropdown], [closeAnyOpenDropdown, goBackToPreviousHotkeyScope],
); );
const openCommandMenu = useCallback(() => { const openCommandMenu = useCallback(() => {

View File

@ -18,7 +18,6 @@ import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRi
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId'; import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId'; import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId'; import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
@ -26,10 +25,6 @@ import { useRecoilCallback } from 'recoil';
export const useCommandMenuCloseAnimationCompleteCleanup = () => { export const useCommandMenuCloseAnimationCompleteCleanup = () => {
const { resetSelectedItem } = useSelectableList('command-menu-list'); const { resetSelectedItem } = useSelectableList('command-menu-list');
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(
COMMAND_MENU_COMPONENT_INSTANCE_ID,
);
const { resetContextStoreStates } = useResetContextStoreStates(); const { resetContextStoreStates } = useResetContextStoreStates();
const { closeDropdown } = useDropdownV2(); const { closeDropdown } = useDropdownV2();
@ -56,7 +51,6 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
set(commandMenuNavigationStackState, []); set(commandMenuNavigationStackState, []);
resetSelectedItem(); resetSelectedItem();
set(hasUserSelectedCommandState, false); set(hasUserSelectedCommandState, false);
goBackToPreviousHotkeyScope();
emitRightDrawerCloseEvent(); emitRightDrawerCloseEvent();
set(isCommandMenuClosingState, false); set(isCommandMenuClosingState, false);
@ -81,12 +75,7 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
); );
} }
}, },
[ [closeDropdown, resetContextStoreStates, resetSelectedItem],
closeDropdown,
goBackToPreviousHotkeyScope,
resetContextStoreStates,
resetSelectedItem,
],
); );
return { return {

View File

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

View File

@ -11,6 +11,9 @@ import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -85,6 +88,11 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
}) })
: []; : [];
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
dropdownId,
);
if (isDisabled === true) { if (isDisabled === true) {
return ( return (
<SelectControl <SelectControl
@ -115,15 +123,31 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
} }
dropdownComponents={ dropdownComponents={
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => ( <SelectableList
<MenuItem hotkeyScope={dropdownId}
key={`select-filter-operand-${index}`} selectableItemIdArray={operandsForFilterType.map(
onClick={() => { (operand) => operand,
handleOperandChange(filterOperand); )}
}} selectableListInstanceId={dropdownId}
text={getOperandLabel(filterOperand)} >
/> {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> </DropdownMenuItemsContainer>
} }
dropdownHotkeyScope={{ scope: dropdownId }} 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 { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; 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 { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display'; import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
@ -72,6 +75,14 @@ export const AdvancedFilterSubFieldSelectMenu = ({
setObjectFilterDropdownIsSelectingCompositeField(false); setObjectFilterDropdownIsSelectingCompositeField(false);
}; };
const { advancedFilterFieldSelectDropdownId } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
advancedFilterFieldSelectDropdownId,
);
if (!isDefined(objectFilterDropdownSubMenuFieldType)) { if (!isDefined(objectFilterDropdownSubMenuFieldType)) {
return null; return null;
} }
@ -86,6 +97,11 @@ export const AdvancedFilterSubFieldSelectMenu = ({
fieldMetadataItemUsedInDropdown.type, fieldMetadataItemUsedInDropdown.type,
); );
const selectableItemIdArray = [
'-1',
...options.map((subFieldName) => subFieldName),
];
return ( return (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -99,35 +115,58 @@ export const AdvancedFilterSubFieldSelectMenu = ({
{getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} {getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItem <SelectableList
key={`select-filter-${-1}`} hotkeyScope={advancedFilterFieldSelectDropdownId}
testId={`select-filter-${-1}`} selectableItemIdArray={selectableItemIdArray}
onClick={() => { selectableListInstanceId={advancedFilterFieldSelectDropdownId}
handleSelectFilter(fieldMetadataItemUsedInDropdown); >
}} <SelectableListItem
LeftIcon={IconApps} itemId={'-1'}
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`} key={`select-filter-${-1}`}
/> onEnter={() => {
{subFieldsAreFilterable && handleSelectFilter(fieldMetadataItemUsedInDropdown);
options.map((subFieldName, index) => ( }}
>
<MenuItem <MenuItem
key={`select-filter-${index}`} focused={selectedItemId === '-1'}
testId={`select-filter-${index}`}
onClick={() => { 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( handleSelectFilter(
fieldMetadataItemUsedInDropdown, fieldMetadataItemUsedInDropdown,
subFieldName, subFieldName,
); );
} }}
}} >
text={getCompositeSubFieldLabel( <MenuItem
objectFilterDropdownSubMenuFieldType, focused={selectedItemId === subFieldName}
subFieldName, key={`select-filter-${index}`}
)} testId={`select-filter-${index}`}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)} onClick={() => {
/> handleSelectFilter(
))} fieldMetadataItemUsedInDropdown,
subFieldName,
);
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
subFieldName,
)}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown?.icon)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</> </>
); );

View File

@ -94,9 +94,6 @@ export const ObjectFilterDropdownBooleanSelect = () => {
selectableListInstanceId="boolean-select" selectableListInstanceId="boolean-select"
selectableItemIdArray={options.map((option) => option.toString())} selectableItemIdArray={options.map((option) => option.toString())}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker} hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
onEnter={(itemId) => {
handleOptionSelect(itemId === 'true');
}}
> >
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{options.map((option) => ( {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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { 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 { 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 { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext'; import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
@ -96,36 +91,6 @@ export const ObjectFilterDropdownFilterSelect = ({
(fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id), (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 = const shouldShowSeparator =
visibleColumnsFieldMetadataItems.length > 0 && visibleColumnsFieldMetadataItems.length > 0 &&
hiddenColumnsFieldMetadataItems.length > 0; hiddenColumnsFieldMetadataItems.length > 0;
@ -137,6 +102,15 @@ export const ObjectFilterDropdownFilterSelect = ({
const { t } = useLingui(); const { t } = useLingui();
const selectableFieldMetadataItemIds = [
...visibleColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
...hiddenColumnsFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
),
];
return ( return (
<> <>
<StyledInput <StyledInput
@ -151,34 +125,21 @@ export const ObjectFilterDropdownFilterSelect = ({
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton} hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableFieldMetadataItemIds} selectableItemIdArray={selectableFieldMetadataItemIds}
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID} selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
> >
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{visibleColumnsFieldMetadataItems.map( {visibleColumnsFieldMetadataItems.map((visibleFieldMetadataItem) => (
(visibleFieldMetadataItem, index) => ( <ObjectFilterDropdownFilterSelectMenuItem
<SelectableItem key={visibleFieldMetadataItem.id}
itemId={visibleFieldMetadataItem.id} fieldMetadataItemToSelect={visibleFieldMetadataItem}
key={`visible-select-filter-${index}`} />
> ))}
<ObjectFilterDropdownFilterSelectMenuItem
fieldMetadataItemToSelect={visibleFieldMetadataItem}
/>
</SelectableItem>
),
)}
{shouldShowSeparator && <DropdownMenuSeparator />} {shouldShowSeparator && <DropdownMenuSeparator />}
{hiddenColumnsFieldMetadataItems.map( {hiddenColumnsFieldMetadataItems.map((hiddenFieldMetadataItem) => (
(hiddenFieldMetadataItem, index) => ( <ObjectFilterDropdownFilterSelectMenuItem
<SelectableItem key={hiddenFieldMetadataItem.id}
itemId={hiddenFieldMetadataItem.id} fieldMetadataItemToSelect={hiddenFieldMetadataItem}
key={`hidden-select-filter-${index}`} />
> ))}
<ObjectFilterDropdownFilterSelectMenuItem
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</SelectableList> </SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />} {shouldShowAdvancedFilterButton && <AdvancedFilterButton />}

View File

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

View File

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

View File

@ -167,12 +167,6 @@ export const ObjectFilterDropdownOptionSelect = () => {
selectableListInstanceId={componentInstanceId} selectableListInstanceId={componentInstanceId}
selectableItemIdArray={objectRecordsIds} selectableItemIdArray={objectRecordsIds}
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker} hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
onEnter={(itemId) => {
const option = optionsInDropdown.find((option) => option.id === itemId);
if (isDefined(option)) {
handleMultipleOptionSelectChange(option, !option.isSelected);
}
}}
> >
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => ( {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 { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu'; import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; 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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@ -74,6 +79,18 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const isDefaultView = currentView?.key === 'INDEX'; const isDefaultView = currentView?.key === 'INDEX';
const nbsp = '\u00A0'; 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 ( return (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -86,81 +103,132 @@ export const ObjectOptionsDropdownLayoutContent = () => {
> >
{t`Layout`} {t`Layout`}
</DropdownMenuHeader> </DropdownMenuHeader>
{!!currentView && ( {!!currentView && (
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItemSelect <SelectableList
LeftIcon={IconTable} selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
text={t`Table`} hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selected={currentView?.type === ViewType.Table} selectableItemIdArray={selectableItemIdArray}
onClick={async () => { >
if (currentView?.type !== ViewType.Table) { <SelectableListItem
await setAndPersistViewType(ViewType.Table); itemId={ViewType.Table}
} onEnter={() => {
}} setAndPersistViewType(ViewType.Table);
/> }}
<MenuItemSelect >
LeftIcon={viewTypeIconMapping(ViewType.Kanban)} <MenuItemSelect
text={t`Kanban`} LeftIcon={IconTable}
disabled={isDefaultView} text={t`Table`}
contextualText={ selected={currentView?.type === ViewType.Table}
isDefaultView ? ( focused={selectedItemId === ViewType.Table}
<> onClick={async () => {
{nbsp}·{nbsp} if (currentView?.type !== ViewType.Table) {
<OverflowingTextWithTooltip await setAndPersistViewType(ViewType.Table);
text={t`Not available for default view`} }
/> }}
</> />
) : availableFieldsForKanban.length === 0 ? ( </SelectableListItem>
t`Create Select...` <SelectableListItem
) : undefined itemId={ViewType.Kanban}
} onEnter={() => {
selected={currentView?.type === ViewType.Kanban} setAndPersistViewType(ViewType.Kanban);
onClick={handleSelectKanbanViewType} }}
/> >
<DropdownMenuSeparator /> <MenuItemSelect
<MenuItem LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
onClick={() => onContentChange('layoutOpenIn')} text={t`Kanban`}
LeftIcon={ disabled={isDefaultView}
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL focused={selectedItemId === ViewType.Kanban}
? IconLayoutSidebarRight contextualText={
: IconLayoutNavbar isDefaultView ? (
} <>
text={t`Open in`} {nbsp}·{nbsp}
contextualText={ <OverflowingTextWithTooltip
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL text={t`Not available for default view`}
? t`Side Panel` />
: t`Record Page` </>
} ) : availableFieldsForKanban.length === 0 ? (
hasSubMenu t`Create Select...`
/> ) : undefined
{currentView?.type === ViewType.Kanban && ( }
<> selected={currentView?.type === ViewType.Kanban}
<MenuItem onClick={handleSelectKanbanViewType}
onClick={() => />
isDefined(recordGroupFieldMetadata) </SelectableListItem>
? onContentChange('recordGroups') <DropdownMenuSeparator />
: onContentChange('recordGroupFields') <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 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 <SelectableListItem
LeftIcon={IconBaselineDensitySmall} itemId={'Compact view'}
onToggleChange={() => onEnter={() => {
setAndPersistIsCompactModeActive( setAndPersistIsCompactModeActive(
!isCompactModeActive, !isCompactModeActive,
currentView, currentView,
) );
} }}
toggled={isCompactModeActive} >
text={t`Compact view`} <MenuItemToggle
toggleSize="small" focused={selectedItemId === 'Compact view'}
/> LeftIcon={IconBaselineDensitySmall}
</> onToggleChange={() =>
)} setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
</>
)}
</SelectableList>
</DropdownMenuItemsContainer> </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 { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions'; import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; 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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; 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 { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
@ -21,6 +27,16 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
const { currentView } = useGetCurrentViewOnly(); const { currentView } = useGetCurrentViewOnly();
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions(); const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
const selectableItemIdArray = [
ViewOpenRecordInType.SIDE_PANEL,
ViewOpenRecordInType.RECORD_PAGE,
];
return ( return (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -34,30 +50,60 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
{t`Open in`} {t`Open in`}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItemSelect <SelectableList
LeftIcon={IconLayoutSidebarRight} selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
text={t`Side Panel`} hotkeyScope={TableOptionsHotkeyScope.Dropdown}
selected={recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL} selectableItemIdArray={selectableItemIdArray}
onClick={() => >
setAndPersistOpenRecordIn( <SelectableListItem
ViewOpenRecordInType.SIDE_PANEL, itemId={ViewOpenRecordInType.SIDE_PANEL}
currentView, onEnter={() =>
) setAndPersistOpenRecordIn(
} ViewOpenRecordInType.SIDE_PANEL,
/> currentView,
<MenuItemSelect )
LeftIcon={IconLayoutNavbar} }
text={t`Record Page`} >
selected={ <MenuItemSelect
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE LeftIcon={IconLayoutSidebarRight}
} text={t`Side Panel`}
onClick={() => selected={
setAndPersistOpenRecordIn( recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
ViewOpenRecordInType.RECORD_PAGE, }
currentView, 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> </DropdownMenuItemsContainer>
</> </>
); );

View File

@ -1,6 +1,7 @@
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName'; 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 { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; 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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -75,92 +79,155 @@ export const ObjectOptionsDropdownMenuContent = () => {
const isDefaultView = currentView?.key === 'INDEX'; 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 ( return (
<> <>
{currentView && ( {currentView && (
<ObjectOptionsDropdownMenuViewName currentView={currentView} /> <ObjectOptionsDropdownMenuViewName currentView={currentView} />
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}> <SelectableList
<MenuItem selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
onClick={() => onContentChange('layout')} hotkeyScope={TableOptionsHotkeyScope.Dropdown}
LeftIcon={viewTypeIconMapping(currentView?.type ?? ViewType.Table)} selectableItemIdArray={selectableItemIdArray}
text={t`Layout`} >
contextualText={`${capitalize(currentView?.type ?? '')}`} <DropdownMenuItemsContainer scrollable={false}>
hasSubMenu <SelectableListItem
/> itemId="Layout"
</DropdownMenuItemsContainer> onEnter={() => onContentChange('layout')}
<DropdownMenuSeparator /> >
<MenuItem
<DropdownMenuItemsContainer scrollable={false}> focused={selectedItemId === 'Layout'}
<MenuItem onClick={() => onContentChange('layout')}
onClick={() => onContentChange('fields')} LeftIcon={viewTypeIconMapping(
LeftIcon={IconListDetails} currentView?.type ?? ViewType.Table,
text={t`Fields`} )}
contextualText={`${visibleBoardFields.length} shown`} text={t`Layout`}
hasSubMenu contextualText={`${capitalize(currentView?.type ?? '')}`}
/> hasSubMenu
/>
<div id="group-by-menu-item"> </SelectableListItem>
<MenuItem </DropdownMenuItemsContainer>
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%"
/>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<MenuItem <DropdownMenuItemsContainer scrollable={false}>
onClick={() => { <SelectableListItem
const currentUrl = window.location.href; itemId="Fields"
navigator.clipboard.writeText(currentUrl); onEnter={() => onContentChange('fields')}
enqueueSnackBar('Link copied to clipboard', { >
variant: SnackBarVariant.Success, <MenuItem
icon: <IconCopy size={theme.icon.size.md} />, focused={selectedItemId === 'Fields'}
duration: 2000, onClick={() => onContentChange('fields')}
}); LeftIcon={IconListDetails}
}} text={t`Fields`}
LeftIcon={IconCopy} contextualText={`${visibleBoardFields.length} shown`}
text={t`Copy link to view`} hasSubMenu
/> />
<div id="delete-view-menu-item"> </SelectableListItem>
<MenuItem
onClick={() => handleDelete()} <div id="group-by-menu-item">
LeftIcon={IconTrash} <SelectableListItem
text={t`Delete view`} itemId="Group"
disabled={currentView?.key === 'INDEX'} onEnter={() =>
/> isDefined(recordGroupFieldMetadata)
</div> ? onContentChange('recordGroups')
{currentView?.key === 'INDEX' && ( : onContentChange('recordGroupFields')
<AppTooltip }
anchorSelect={`#delete-view-menu-item`} >
content={t`Not available on Default View`} <MenuItem
noArrow focused={selectedItemId === 'Group'}
place="bottom" onClick={() =>
width="100%" isDefined(recordGroupFieldMetadata)
/> ? onContentChange('recordGroups')
)} : onContentChange('recordGroupFields')
</DropdownMenuItemsContainer> }
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 { 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 { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; 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 { 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 { 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 { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { import {
IconChevronLeft, IconChevronLeft,
IconHandMove, IconHandMove,
@ -32,6 +37,11 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
setRecordGroupSort(sort); setRecordGroupSort(sort);
}; };
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
useEffect(() => { useEffect(() => {
if ( if (
currentContentId === 'hiddenRecordGroups' && currentContentId === 'hiddenRecordGroups' &&
@ -41,6 +51,12 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
} }
}, [hiddenRecordGroupIds, currentContentId, onContentChange]); }, [hiddenRecordGroupIds, currentContentId, onContentChange]);
const selectableItemIdArray = [
RecordGroupSort.Manual,
RecordGroupSort.Alphabetical,
RecordGroupSort.ReverseAlphabetical,
];
return ( return (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -54,28 +70,58 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
Sort Sort
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItemSelect <SelectableList
onClick={() => handleRecordGroupSortChange(RecordGroupSort.Manual)} selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
LeftIcon={IconHandMove} hotkeyScope={TableOptionsHotkeyScope.Dropdown}
text={RecordGroupSort.Manual} selectableItemIdArray={selectableItemIdArray}
selected={recordGroupSort === RecordGroupSort.Manual} >
/> <SelectableListItem
<MenuItemSelect itemId={RecordGroupSort.Manual}
onClick={() => onEnter={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
handleRecordGroupSortChange(RecordGroupSort.Alphabetical) >
} <MenuItemSelect
LeftIcon={IconSortAZ} onClick={() =>
text={RecordGroupSort.Alphabetical} handleRecordGroupSortChange(RecordGroupSort.Manual)
selected={recordGroupSort === RecordGroupSort.Alphabetical} }
/> LeftIcon={IconHandMove}
<MenuItemSelect text={RecordGroupSort.Manual}
onClick={() => selected={recordGroupSort === RecordGroupSort.Manual}
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical) focused={selectedItemId === RecordGroupSort.Manual}
} />
LeftIcon={IconSortZA} </SelectableListItem>
text={RecordGroupSort.ReverseAlphabetical} <SelectableListItem
selected={recordGroupSort === RecordGroupSort.ReverseAlphabetical} 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> </DropdownMenuItemsContainer>
</> </>
); );

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/r
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; 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 { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody'; import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader'; import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
@ -121,7 +122,9 @@ export const RecordBoardCard = () => {
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY,
}); });
openDropdown(actionMenuDropdownId); openDropdown(actionMenuDropdownId, {
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
});
}; };
const handleCardClick = () => { 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 { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; 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 { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display'; import { IconPlus } from 'twenty-ui/display';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%; height: 100%;
width: 100%; width: 100%;
`; `;

View File

@ -3,10 +3,10 @@ import styled from '@emotion/styled';
import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId'; import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId';
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent'; import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; 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'; import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%; height: 100%;
width: 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 { 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 { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; 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 { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; 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 { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { SearchRecord } from '~/generated-metadata/graphql'; import { SearchRecord } from '~/generated-metadata/graphql';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%; height: 100%;
width: 100%; width: 100%;
`; `;
@ -62,6 +62,7 @@ export const MultipleRecordPickerMenuItemContent = ({
<StyledSelectableItem <StyledSelectableItem
itemId={searchRecord.recordId} itemId={searchRecord.recordId}
key={searchRecord.recordId} key={searchRecord.recordId}
onEnter={() => handleSelectChange(!isRecordSelectedWithObjectItem)}
> >
<MenuItemMultiSelectAvatar <MenuItemMultiSelectAvatar
onSelectChange={(isSelected) => handleSelectChange(isSelected)} 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 { 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 { 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 { 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 { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId'; import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; 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 { 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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableItem)` export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%; height: 100%;
width: 100%; width: 100%;
`; `;
@ -45,11 +43,6 @@ export const MultipleRecordPickerMenuItems = ({
const { resetSelectedItem } = useSelectableList( const { resetSelectedItem } = useSelectableList(
selectableListComponentInstanceId, selectableListComponentInstanceId,
); );
const singlePickableMorphItemFamilySelector =
useRecoilComponentCallbackStateV2(
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
componentInstanceId,
);
const multipleRecordPickerPickableMorphItemsState = const multipleRecordPickerPickableMorphItemsState =
useRecoilComponentCallbackStateV2( useRecoilComponentCallbackStateV2(
@ -82,42 +75,12 @@ export const MultipleRecordPickerMenuItems = ({
[multipleRecordPickerPickableMorphItemsState], [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 ( return (
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
<SelectableList <SelectableList
selectableListInstanceId={selectableListComponentInstanceId} selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={pickableRecordIds} selectableItemIdArray={pickableRecordIds}
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker} hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
onEnter={handleEnter}
> >
{pickableRecordIds.map((recordId) => { {pickableRecordIds.map((recordId) => {
return ( 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 { 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 { 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 { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; 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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;
export const MultipleRecordPickerSearchInput = () => { export const MultipleRecordPickerSearchInput = () => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow( const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
MultipleRecordPickerComponentInstanceContext, 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 { 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 { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId'; 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 { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
@ -16,7 +16,7 @@ type SingleRecordPickerMenuItemProps = {
selectedRecord?: SingleRecordPickerRecord; selectedRecord?: SingleRecordPickerRecord;
}; };
const StyledSelectableItem = styled(SelectableItem)` const StyledSelectableItem = styled(SelectableListItem)`
width: 100%; width: 100%;
`; `;
@ -40,14 +40,19 @@ export const SingleRecordPickerMenuItem = ({
); );
return ( return (
<StyledSelectableItem itemId={record.id} key={record.id}> <StyledSelectableItem
itemId={record.id}
key={record.id}
onEnter={() => {
onRecordSelected(record);
}}
>
<MenuItemSelectAvatar <MenuItemSelectAvatar
key={record.id}
testId="menu-item" testId="menu-item"
onClick={() => onRecordSelected(record)} onClick={() => onRecordSelected(record)}
text={record.name} text={record.name}
selected={selectedRecord?.id === record.id} selected={selectedRecord?.id === record.id}
hovered={isSelectedItemId} focused={isSelectedItemId}
avatar={ avatar={
<Avatar <Avatar
avatarUrl={record.avatarUrl} 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 { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; 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 { 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 { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
@ -108,14 +109,6 @@ export const SingleRecordPickerMenuItems = ({
selectableListInstanceId={selectableListComponentInstanceId} selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIds} selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const recordIndex = recordsInDropdown.findIndex(
(record) => record.id === itemId,
);
setSelectedRecordId(itemId);
onRecordSelected(recordsInDropdown[recordIndex]);
resetSelectedItem();
}}
> >
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{loading && !isFiltered ? ( {loading && !isFiltered ? (
@ -128,17 +121,25 @@ export const SingleRecordPickerMenuItems = ({
case 'select-none': { case 'select-none': {
return ( return (
emptyLabel && ( emptyLabel && (
<MenuItemSelect <SelectableListItem
key={record.id} key={record.id}
onClick={() => { itemId={record.id}
onEnter={() => {
setSelectedRecordId(undefined); setSelectedRecordId(undefined);
onRecordSelected(); onRecordSelected();
}} }}
LeftIcon={EmptyIcon} >
text={emptyLabel} <MenuItemSelect
selected={isUndefined(selectedRecordId)} onClick={() => {
hovered={isSelectedSelectNoneButton} 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 { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; 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 { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useTriggerActionMenuDropdown = ({ export const useTriggerActionMenuDropdown = ({
recordTableId, recordTableId,
}: { }: {
@ -25,18 +25,17 @@ export const useTriggerActionMenuDropdown = ({
recordTableId, recordTableId,
); );
const actionMenuDropdownId =
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId);
const recordIndexActionMenuDropdownPositionState = extractComponentState( const recordIndexActionMenuDropdownPositionState = extractComponentState(
recordIndexActionMenuDropdownPositionComponentState, recordIndexActionMenuDropdownPositionComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId), actionMenuDropdownId,
); );
const isActionMenuDropdownOpenState = extractComponentState( const { openDropdown } = useDropdown(actionMenuDropdownId);
isDropdownOpenComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId),
);
const { setActiveDropdownFocusIdAndMemorizePrevious } = const { closeCommandMenu } = useCommandMenu();
useSetActiveDropdownFocusIdAndMemorizePrevious();
const triggerActionMenuDropdown = useRecoilCallback( const triggerActionMenuDropdown = useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
@ -57,19 +56,17 @@ export const useTriggerActionMenuDropdown = ({
set(isRowSelectedFamilyState(recordId), true); set(isRowSelectedFamilyState(recordId), true);
} }
set(isActionMenuDropdownOpenState, true); closeCommandMenu();
const actionMenuDropdownId = openDropdown({
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId); scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
});
setActiveDropdownFocusIdAndMemorizePrevious(actionMenuDropdownId);
}, },
[ [
isActionMenuDropdownOpenState,
isRowSelectedFamilyState,
recordIndexActionMenuDropdownPositionState, recordIndexActionMenuDropdownPositionState,
setActiveDropdownFocusIdAndMemorizePrevious, isRowSelectedFamilyState,
actionMenuInstanceId, 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; 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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -93,43 +94,38 @@ export const MultipleSelectDropdown = ({
selectableListInstanceId={selectableListId} selectableListInstanceId={selectableListId}
selectableItemIdArray={selectableItemIds} selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope} 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> <DropdownMenuItemsContainer hasMaxHeight>
{itemsInDropdown?.map((item) => { {itemsInDropdown?.map((item) => {
return ( return (
<MenuItemMultiSelectAvatar <SelectableListItem
key={item.id} itemId={item.id}
selected={item.isSelected} onEnter={() => {
isKeySelected={item.id === selectedItemId}
onSelectChange={(newCheckedValue) => {
resetSelectedItem(); resetSelectedItem();
handleItemSelectChange(item, newCheckedValue); handleItemSelectChange(item, !item.isSelected);
}} }}
avatar={ >
<StyledMultipleSelectDropdownAvatarChip <MenuItemMultiSelectAvatar
className="avatar-icon-container" key={item.id}
name={item.name} selected={item.isSelected}
avatarUrl={item.avatarUrl} isKeySelected={item.id === selectedItemId}
LeftIcon={item.AvatarIcon} onSelectChange={(newCheckedValue) => {
avatarType={item.avatarType} resetSelectedItem();
isIconInverted={item.isIconInverted} handleItemSelectChange(item, newCheckedValue);
placeholderColorSeed={item.id} }}
/> 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" />} {showNoResult && <MenuItem text="No results" />}

View File

@ -101,14 +101,6 @@ export const MultiSelectInput = ({
selectableListInstanceId={selectableListComponentInstanceId} selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={optionIds} selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const option = filteredOptionsInDropDown.find(
(option) => option.value === itemId,
);
if (isDefined(option)) {
onOptionSelected(formatNewSelectedOptions(option.value));
}
}}
> >
<DropdownMenu data-select-disable ref={containerRef}> <DropdownMenu data-select-disable ref={containerRef}>
<DropdownMenuSearchInput <DropdownMenuSearchInput

View File

@ -20,7 +20,6 @@ export const SelectInput = ({
selectableListComponentInstanceId, selectableListComponentInstanceId,
selectableItemIdArray, selectableItemIdArray,
hotkeyScope, hotkeyScope,
onEnter,
onOptionSelected, onOptionSelected,
options, options,
onCancel, onCancel,
@ -34,7 +33,6 @@ export const SelectInput = ({
selectableListInstanceId={selectableListComponentInstanceId} selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIdArray} selectableItemIdArray={selectableItemIdArray}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onEnter={onEnter}
> >
<SelectBaseInput <SelectBaseInput
onOptionSelected={onOptionSelected} onOptionSelected={onOptionSelected}

View File

@ -11,6 +11,7 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { arrayToChunks } from '~/utils/array/arrayToChunks'; import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { useSelectableListListenToEnterHotkeyOnItem } from '@/ui/layout/selectable-list/hooks/useSelectableListListenToEnterHotkeyOnItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
@ -22,7 +23,6 @@ import {
LightIconButton, LightIconButton,
} from 'twenty-ui/input'; } from 'twenty-ui/input';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope'; import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
export type IconPickerProps = { export type IconPickerProps = {
disabled?: boolean; disabled?: boolean;
dropdownId?: string; dropdownId?: string;
@ -69,6 +69,12 @@ const IconPickerIcon = ({
iconKey, iconKey,
); );
useSelectableListListenToEnterHotkeyOnItem({
hotkeyScope: IconPickerHotkeyScope.IconPicker,
itemId: iconKey,
onEnter: onClick,
});
return ( return (
<StyledLightIconButton <StyledLightIconButton
key={iconKey} key={iconKey}
@ -179,10 +185,6 @@ export const IconPicker = ({
selectableListInstanceId="icon-list" selectableListInstanceId="icon-list"
selectableItemIdMatrix={iconKeys2d} selectableItemIdMatrix={iconKeys2d}
hotkeyScope={IconPickerHotkeyScope.IconPicker} hotkeyScope={IconPickerHotkeyScope.IconPicker}
onEnter={(iconKey) => {
onChange({ iconKey, Icon: getIcon(iconKey) });
closeDropdown();
}}
> >
<DropdownMenu width={176}> <DropdownMenu width={176}>
<DropdownMenuSearchInput <DropdownMenuSearchInput

View File

@ -9,6 +9,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { SelectControl } from '@/ui/input/components/SelectControl'; import { SelectControl } from '@/ui/input/components/SelectControl';
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset'; import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
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 { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconComponent } from 'twenty-ui/display'; import { IconComponent } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input'; import { SelectOption } from 'twenty-ui/input';
@ -109,6 +113,13 @@ export const Select = <Value extends SelectValue>({
? selectContainerRef.current?.clientWidth ? selectContainerRef.current?.clientWidth
: dropdownWidth; : dropdownWidth;
const selectableItemIdArray = filteredOptions.map((option) => option.label);
const selectedItemId = useRecoilComponentValueV2(
selectedItemIdComponentState,
dropdownId,
);
return ( return (
<StyledContainer <StyledContainer
className={className} className={className}
@ -153,20 +164,36 @@ export const Select = <Value extends SelectValue>({
)} )}
{!!filteredOptions.length && ( {!!filteredOptions.length && (
<DropdownMenuItemsContainer hasMaxHeight width={'auto'}> <DropdownMenuItemsContainer hasMaxHeight width={'auto'}>
{filteredOptions.map((option) => ( <SelectableList
<MenuItemSelect hotkeyScope={SelectHotkeyScope.Select}
key={`${option.value}-${option.label}`} selectableListInstanceId={dropdownId}
LeftIcon={option.Icon} selectableItemIdArray={selectableItemIdArray}
text={option.label} >
selected={selectedOption.value === option.value} {filteredOptions.map((option) => (
needIconCheck={needIconCheck} <SelectableListItem
onClick={() => { key={`${option.value}-${option.label}`}
onChange?.(option.value); itemId={option.label}
onBlur?.(); onEnter={() => {
closeDropdown(); onChange?.(option.value);
}} onBlur?.();
/> closeDropdown();
))} }}
>
<MenuItemSelect
LeftIcon={option.Icon}
text={option.label}
selected={selectedOption.value === option.value}
focused={selectedItemId === option.label}
needIconCheck={needIconCheck}
onClick={() => {
onChange?.(option.value);
onBlur?.();
closeDropdown();
}}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
)} )}
{!!callToActionButton && !!filteredOptions.length && ( {!!callToActionButton && !!filteredOptions.length && (

View File

@ -7,9 +7,9 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelectTag } from 'twenty-ui/navigation';
import { SelectOption } from 'twenty-ui/input';
import { TagColor } from 'twenty-ui/components'; import { TagColor } from 'twenty-ui/components';
import { SelectOption } from 'twenty-ui/input';
import { MenuItemSelectTag } from 'twenty-ui/navigation';
interface SelectInputProps { interface SelectInputProps {
onOptionSelected: (selectedOption: SelectOption) => void; onOptionSelected: (selectedOption: SelectOption) => void;
@ -107,7 +107,6 @@ export const SelectInput = ({
{onClear && clearLabel && ( {onClear && clearLabel && (
<MenuItemSelectTag <MenuItemSelectTag
key={`No ${clearLabel}`} key={`No ${clearLabel}`}
selected={false}
text={`No ${clearLabel}`} text={`No ${clearLabel}`}
color="transparent" color="transparent"
variant={'outline'} variant={'outline'}
@ -121,7 +120,7 @@ export const SelectInput = ({
return ( return (
<MenuItemSelectTag <MenuItemSelectTag
key={option.value} key={option.value}
selected={selectedOption?.value === option.value} focused={selectedOption?.value === option.value}
text={option.label} text={option.label}
color={(option.color as TagColor) ?? 'transparent'} color={(option.color as TagColor) ?? 'transparent'}
onClick={() => handleOptionChange(option)} onClick={() => handleOptionChange(option)}

View File

@ -2,8 +2,8 @@ import { ReactNode, useEffect } from 'react';
import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys'; import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys';
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext'; import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { SelectableListContextProvider } from '@/ui/layout/selectable-list/states/contexts/SelectableListContext';
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState'; import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
import { selectableListOnEnterComponentState } from '@/ui/layout/selectable-list/states/selectableListOnEnterComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { arrayToChunks } from '~/utils/array/arrayToChunks'; import { arrayToChunks } from '~/utils/array/arrayToChunks';
@ -14,7 +14,6 @@ type SelectableListProps = {
selectableItemIdMatrix?: string[][]; selectableItemIdMatrix?: string[][];
onSelect?: (selected: string) => void; onSelect?: (selected: string) => void;
hotkeyScope: string; hotkeyScope: string;
onEnter?: (itemId: string) => void;
selectableListInstanceId: string; selectableListInstanceId: string;
}; };
@ -24,25 +23,15 @@ export const SelectableList = ({
selectableItemIdArray, selectableItemIdArray,
selectableItemIdMatrix, selectableItemIdMatrix,
selectableListInstanceId, selectableListInstanceId,
onEnter,
onSelect, onSelect,
}: SelectableListProps) => { }: SelectableListProps) => {
useSelectableListHotKeys(selectableListInstanceId, hotkeyScope, onSelect); useSelectableListHotKeys(selectableListInstanceId, hotkeyScope, onSelect);
const setSelectableListOnEnter = useSetRecoilComponentStateV2(
selectableListOnEnterComponentState,
selectableListInstanceId,
);
const setSelectableItemIds = useSetRecoilComponentStateV2( const setSelectableItemIds = useSetRecoilComponentStateV2(
selectableItemIdsComponentState, selectableItemIdsComponentState,
selectableListInstanceId, selectableListInstanceId,
); );
useEffect(() => {
setSelectableListOnEnter(() => onEnter);
}, [onEnter, setSelectableListOnEnter]);
useEffect(() => { useEffect(() => {
if (!selectableItemIdArray && !selectableItemIdMatrix) { if (!selectableItemIdArray && !selectableItemIdMatrix) {
throw new Error( throw new Error(
@ -65,7 +54,9 @@ export const SelectableList = ({
instanceId: selectableListInstanceId, instanceId: selectableListInstanceId,
}} }}
> >
{children} <SelectableListContextProvider value={{ hotkeyScope }}>
{children}
</SelectableListContextProvider>
</SelectableListComponentInstanceContext.Provider> </SelectableListComponentInstanceContext.Provider>
); );
}; };

View File

@ -1,25 +1,29 @@
import { ReactNode, useEffect, useRef } from 'react'; import { ReactNode, useEffect, useRef } from 'react';
import { SelectableListItemHotkeyEffect } from '@/ui/layout/selectable-list/components/SelectableListItemHotkeyEffect';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div` const StyledContainer = styled.div`
height: 100%; height: 100%;
width: 100%; width: 100%;
`; `;
export type SelectableItemProps = { export type SelectableListItemProps = {
itemId: string; itemId: string;
children: ReactNode; children: ReactNode;
className?: string; className?: string;
onEnter?: () => void;
}; };
export const SelectableItem = ({ export const SelectableListItem = ({
itemId, itemId,
children, children,
className, className,
}: SelectableItemProps) => { onEnter,
}: SelectableListItemProps) => {
const isSelectedItemId = useRecoilComponentFamilyValueV2( const isSelectedItemId = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector, isSelectedItemIdComponentFamilySelector,
itemId, itemId,
@ -34,8 +38,13 @@ export const SelectableItem = ({
}, [isSelectedItemId]); }, [isSelectedItemId]);
return ( return (
<StyledContainer className={className} ref={scrollRef}> <>
{children} {isSelectedItemId && isDefined(onEnter) && (
</StyledContainer> <SelectableListItemHotkeyEffect itemId={itemId} onEnter={onEnter} />
)}
<StyledContainer className={className} ref={scrollRef}>
{children}
</StyledContainer>
</>
); );
}; };

View File

@ -0,0 +1,19 @@
import { useSelectableListListenToEnterHotkeyOnItem } from '@/ui/layout/selectable-list/hooks/useSelectableListListenToEnterHotkeyOnItem';
import { useSelectableListContextOrThrow } from '@/ui/layout/selectable-list/states/contexts/SelectableListContext';
export const SelectableListItemHotkeyEffect = ({
itemId,
onEnter,
}: {
itemId: string;
onEnter: () => void;
}) => {
const { hotkeyScope } = useSelectableListContextOrThrow();
useSelectableListListenToEnterHotkeyOnItem({
hotkeyScope,
itemId,
onEnter,
});
return null;
};

View File

@ -3,7 +3,6 @@ import { useRecoilCallback } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState'; import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
import { selectableListOnEnterComponentState } from '@/ui/layout/selectable-list/states/selectableListOnEnterComponentState';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -147,35 +146,4 @@ export const useSelectableListHotKeys = (
hotkeyScope, hotkeyScope,
[], [],
); );
useScopedHotkeys(
Key.Enter,
useRecoilCallback(
({ snapshot }) =>
() => {
const selectedItemId = getSnapshotValue(
snapshot,
selectedItemIdComponentState.atomFamily({
instanceId: instanceId,
}),
);
const onEnter = getSnapshotValue(
snapshot,
selectableListOnEnterComponentState.atomFamily({
instanceId: instanceId,
}),
);
if (isNonEmptyString(selectedItemId)) {
onEnter?.(selectedItemId);
}
},
[instanceId],
),
hotkeyScope,
[],
{
preventDefault: false,
},
);
}; };

View File

@ -39,8 +39,5 @@ export const useSelectableListListenToEnterHotkeyOnItem = ({
), ),
hotkeyScope, hotkeyScope,
[itemId, onEnter], [itemId, onEnter],
{
preventDefault: false,
},
); );
}; };

View File

@ -0,0 +1,8 @@
import { createRequiredContext } from '~/utils/createRequiredContext';
export type SelectableListContextValue = {
hotkeyScope: string;
};
export const [SelectableListContextProvider, useSelectableListContextOrThrow] =
createRequiredContext<SelectableListContextValue>('SelectableListContext');

View File

@ -1,10 +0,0 @@
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const selectableListOnEnterComponentState = createComponentStateV2<
((itemId: string) => void) | undefined
>({
key: 'selectableListOnEnterComponentState',
defaultValue: undefined,
componentInstanceContext: SelectableListComponentInstanceContext,
});

View File

@ -141,7 +141,7 @@ export const WorkflowVariablesDropdownFieldItems = ({
<MenuItemSelect <MenuItemSelect
key={key} key={key}
selected={false} selected={false}
hovered={false} focused={false}
onClick={() => handleSelectField(key)} onClick={() => handleSelectField(key)}
text={subStep.label || key} text={subStep.label || key}
hasSubMenu={!subStep.isLeaf} hasSubMenu={!subStep.isLeaf}

View File

@ -121,7 +121,7 @@ export const WorkflowVariablesDropdownObjectItems = ({
{shouldDisplaySubStepObject && displayedSubStepObject?.label && ( {shouldDisplaySubStepObject && displayedSubStepObject?.label && (
<MenuItemSelect <MenuItemSelect
selected={false} selected={false}
hovered={false} focused={false}
onClick={handleSelectObject} onClick={handleSelectObject}
text={displayedSubStepObject.label} text={displayedSubStepObject.label}
hasSubMenu={false} hasSubMenu={false}
@ -136,7 +136,7 @@ export const WorkflowVariablesDropdownObjectItems = ({
<MenuItemSelect <MenuItemSelect
key={key} key={key}
selected={false} selected={false}
hovered={false} focused={false}
onClick={() => handleSelectField(key)} onClick={() => handleSelectField(key)}
text={value.label || key} text={value.label || key}
hasSubMenu={!value.isLeaf} hasSubMenu={!value.isLeaf}

View File

@ -55,7 +55,7 @@ export const WorkflowVariablesDropdownWorkflowStepItems = ({
<MenuItemSelect <MenuItemSelect
key={`step-${item.id}`} key={`step-${item.id}`}
selected={false} selected={false}
hovered={false} focused={false}
onClick={() => onSelect(item.id)} onClick={() => onSelect(item.id)}
text={item.name} text={item.name}
LeftIcon={item.icon ? getIcon(item.icon) : undefined} LeftIcon={item.icon ? getIcon(item.icon) : undefined}

View File

@ -35,6 +35,7 @@ export type MenuItemProps = {
text: ReactNode; text: ReactNode;
contextualText?: ReactNode; contextualText?: ReactNode;
hasSubMenu?: boolean; hasSubMenu?: boolean;
focused?: boolean;
}; };
export const MenuItem = ({ export const MenuItem = ({
@ -53,6 +54,7 @@ export const MenuItem = ({
contextualText, contextualText,
hasSubMenu = false, hasSubMenu = false,
disabled = false, disabled = false,
focused = false,
}: MenuItemProps) => { }: MenuItemProps) => {
const theme = useTheme(); const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
@ -75,6 +77,7 @@ export const MenuItem = ({
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly} isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
focused={focused}
> >
<StyledMenuItemLeftContent> <StyledMenuItemLeftContent>
<MenuItemLeftContent <MenuItemLeftContent

View File

@ -7,19 +7,11 @@ import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase'; import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{ export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
selected: boolean;
disabled?: boolean; disabled?: boolean;
hovered?: boolean; focused?: boolean;
}>` }>`
${({ theme, selected, disabled, hovered }) => { ${({ theme, disabled, focused }) => {
if (selected) { if (disabled === true) {
return css`
background: ${theme.background.transparent.light};
&:hover {
background: ${theme.background.transparent.medium};
}
`;
} else if (disabled === true) {
return css` return css`
background: inherit; background: inherit;
&:hover { &:hover {
@ -30,7 +22,7 @@ export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
cursor: default; cursor: default;
`; `;
} else if (hovered === true) { } else if (focused === true) {
return css` return css`
background: ${theme.background.transparent.light}; background: ${theme.background.transparent.light};
`; `;
@ -46,7 +38,7 @@ type MenuItemSelectProps = {
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
hovered?: boolean; focused?: boolean;
hasSubMenu?: boolean; hasSubMenu?: boolean;
contextualText?: ReactNode; contextualText?: ReactNode;
}; };
@ -59,7 +51,7 @@ export const MenuItemSelect = ({
className, className,
onClick, onClick,
disabled, disabled,
hovered, focused,
hasSubMenu = false, hasSubMenu = false,
contextualText, contextualText,
}: MenuItemSelectProps) => { }: MenuItemSelectProps) => {
@ -69,9 +61,8 @@ export const MenuItemSelect = ({
<StyledMenuItemSelect <StyledMenuItemSelect
onClick={onClick} onClick={onClick}
className={className} className={className}
selected={selected}
disabled={disabled} disabled={disabled}
hovered={hovered} focused={focused}
role="option" role="option"
aria-selected={selected} aria-selected={selected}
aria-disabled={disabled} aria-disabled={disabled}

View File

@ -17,7 +17,7 @@ type MenuItemSelectAvatarProps = {
className?: string; className?: string;
onClick?: (event?: React.MouseEvent) => void; onClick?: (event?: React.MouseEvent) => void;
disabled?: boolean; disabled?: boolean;
hovered?: boolean; focused?: boolean;
testId?: string; testId?: string;
}; };
@ -28,7 +28,7 @@ export const MenuItemSelectAvatar = ({
className, className,
onClick, onClick,
disabled, disabled,
hovered, focused,
testId, testId,
}: MenuItemSelectAvatarProps) => { }: MenuItemSelectAvatarProps) => {
const theme = useTheme(); const theme = useTheme();
@ -37,9 +37,8 @@ export const MenuItemSelectAvatar = ({
<StyledMenuItemSelect <StyledMenuItemSelect
onClick={onClick} onClick={onClick}
className={className} className={className}
selected={selected}
disabled={disabled} disabled={disabled}
hovered={hovered} focused={focused}
data-testid={testId} data-testid={testId}
role="option" role="option"
aria-selected={selected} aria-selected={selected}

View File

@ -15,7 +15,7 @@ type MenuItemSelectColorProps = {
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
hovered?: boolean; focused?: boolean;
color: ThemeColor; color: ThemeColor;
variant?: ColorSampleVariant; variant?: ColorSampleVariant;
}; };
@ -39,7 +39,7 @@ export const MenuItemSelectColor = ({
className, className,
onClick, onClick,
disabled, disabled,
hovered, focused,
variant = 'default', variant = 'default',
}: MenuItemSelectColorProps) => { }: MenuItemSelectColorProps) => {
const theme = useTheme(); const theme = useTheme();
@ -48,9 +48,8 @@ export const MenuItemSelectColor = ({
<StyledMenuItemSelect <StyledMenuItemSelect
onClick={onClick} onClick={onClick}
className={className} className={className}
selected={selected}
disabled={disabled} disabled={disabled}
hovered={hovered} focused={focused}
> >
<StyledMenuItemLeftContent> <StyledMenuItemLeftContent>
<ColorSample colorName={color} variant={variant} /> <ColorSample colorName={color} variant={variant} />

View File

@ -11,7 +11,8 @@ import { ThemeColor } from '@ui/theme';
import { StyledMenuItemSelect } from './MenuItemSelect'; import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectTagProps = { type MenuItemSelectTagProps = {
selected: boolean; selected?: boolean;
focused?: boolean;
isKeySelected?: boolean; isKeySelected?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
@ -24,6 +25,7 @@ type MenuItemSelectTagProps = {
export const MenuItemSelectTag = ({ export const MenuItemSelectTag = ({
color, color,
selected, selected,
focused,
isKeySelected, isKeySelected,
className, className,
onClick, onClick,
@ -36,7 +38,7 @@ export const MenuItemSelectTag = ({
<StyledMenuItemSelect <StyledMenuItemSelect
onClick={onClick} onClick={onClick}
className={className} className={className}
selected={selected} focused={focused}
isKeySelected={isKeySelected} isKeySelected={isKeySelected}
> >
<StyledMenuItemLeftContent> <StyledMenuItemLeftContent>

View File

@ -17,6 +17,7 @@ const StyledToggleContainer = styled.label`
`; `;
type MenuItemToggleProps = { type MenuItemToggleProps = {
focused?: boolean;
LeftIcon?: IconComponent; LeftIcon?: IconComponent;
toggled: boolean; toggled: boolean;
text: string; text: string;
@ -26,6 +27,7 @@ type MenuItemToggleProps = {
}; };
export const MenuItemToggle = ({ export const MenuItemToggle = ({
focused,
LeftIcon, LeftIcon,
text, text,
toggled, toggled,
@ -35,7 +37,7 @@ export const MenuItemToggle = ({
}: MenuItemToggleProps) => { }: MenuItemToggleProps) => {
const inputId = useId(); const inputId = useId();
return ( return (
<StyledMenuItemBase className={className}> <StyledMenuItemBase className={className} focused={focused}>
<StyledToggleContainer htmlFor={inputId}> <StyledToggleContainer htmlFor={inputId}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} /> <MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<StyledMenuItemRightContent> <StyledMenuItemRightContent>

View File

@ -13,6 +13,7 @@ export type MenuItemBaseProps = {
isHoverBackgroundDisabled?: boolean; isHoverBackgroundDisabled?: boolean;
hovered?: boolean; hovered?: boolean;
disabled?: boolean; disabled?: boolean;
focused?: boolean;
}; };
export const StyledMenuItemBase = styled.div<MenuItemBaseProps>` export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
@ -72,6 +73,12 @@ export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
} }
}} }}
${({ focused, theme }) =>
focused &&
css`
background: ${theme.background.transparent.light};
`};
position: relative; position: relative;
user-select: none; user-select: none;