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
co-authored by Lucas Bordeau
parent 0b1b81429e
commit f201091c68
61 changed files with 1195 additions and 761 deletions
@@ -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>
); );
}; };
@@ -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>
); );
}; };
@@ -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={{
@@ -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;
@@ -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>
} }
/> />
@@ -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>
} }
@@ -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>
@@ -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);
}} }}
@@ -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>
); );
}; };
@@ -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);
});
}); });
@@ -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(
@@ -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(() => {
@@ -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 {
@@ -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>
@@ -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 }}
@@ -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>
</> </>
); );
@@ -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) => (
@@ -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 />}
@@ -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>
</> </>
); );
@@ -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>
); );
}; };
@@ -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}
@@ -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) => (
@@ -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}
onClick={handleSelectKanbanViewType}
/>
</SelectableListItem>
<DropdownMenuSeparator />
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}
onEnter={() => {
onContentChange('layoutOpenIn');
}}
>
<MenuItem <MenuItem
onClick={() => focused={selectedItemId === ViewOpenRecordInType.SIDE_PANEL}
isDefined(recordGroupFieldMetadata) LeftIcon={
? onContentChange('recordGroups') recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
: onContentChange('recordGroupFields') ? 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>
)} )}
</> </>
@@ -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>
</> </>
); );
@@ -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>
</> </>
); );
}; };
@@ -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>
</> </>
); );
@@ -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>
</> </>
)} )}
@@ -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}
/> />
@@ -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 {
@@ -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 = () => {
@@ -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%;
`; `;
@@ -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%;
`; `;
@@ -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)}
@@ -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 (
@@ -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,
@@ -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}
@@ -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>
) )
); );
} }
@@ -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,
], ],
); );
@@ -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" />}
@@ -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
@@ -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}
@@ -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
@@ -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 && (
@@ -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)}
@@ -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>
); );
}; };
@@ -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>
</>
); );
}; };
@@ -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;
};
@@ -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,
},
);
}; };
@@ -39,8 +39,5 @@ export const useSelectableListListenToEnterHotkeyOnItem = ({
), ),
hotkeyScope, hotkeyScope,
[itemId, onEnter], [itemId, onEnter],
{
preventDefault: false,
},
); );
}; };
@@ -0,0 +1,8 @@
import { createRequiredContext } from '~/utils/createRequiredContext';
export type SelectableListContextValue = {
hotkeyScope: string;
};
export const [SelectableListContextProvider, useSelectableListContextOrThrow] =
createRequiredContext<SelectableListContextValue>('SelectableListContext');
@@ -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,
});
@@ -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}
@@ -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}
@@ -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}
@@ -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
@@ -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}
@@ -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}
@@ -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} />
@@ -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>
@@ -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>
@@ -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;