Use SelectableList in RelationPicker, SingleEntitySelectBase and MultipleEntitySelect (#2949)

* 2747-fix: conditional updation of selectedItemId

* 2747-fix: bug in toggling

* 2747-feat: SingleEntitySelectBase list changed to SelectableList

* 2747-feat: MultipleEntitySelect use SelectableList

* Fix lint

* 2747-fix: onEnter property fix for SingleEntitySelectBase

* 2747-fix: onEnter property fix for MultipleEntitySelect

* yarn fix in twenty-front

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Kanav Arora
2023-12-14 16:40:58 +05:30
committed by GitHub
parent 4673a302c7
commit ed2cd408bf
10 changed files with 297 additions and 75 deletions

View File

@ -1,11 +1,16 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import debounce from 'lodash.debounce';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -71,6 +76,8 @@ export const MultipleEntitySelect = <
},
});
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
return (
<DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuSearchInput
@ -80,25 +87,46 @@ export const MultipleEntitySelect = <
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{entitiesInDropdown?.map((entity) => (
<MenuItemMultiSelectAvatar
key={entity.id}
selected={value[entity.id]}
onSelectChange={(newCheckedValue) =>
onChange({ ...value, [entity.id]: newCheckedValue })
<SelectableList
selectableListId="multiple-entity-select-list"
selectableItemIds={[selectableItemIds]}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(_itemId) => {
if (_itemId in value === false || value[_itemId] === false) {
onChange({ ...value, [_itemId]: true });
} else {
onChange({ ...value, [_itemId]: false });
}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
}}
>
{entitiesInDropdown?.map((entity) => (
<SelectableItem itemId={entity.id} key={entity.id}>
<MenuItemMultiSelectAvatar
key={entity.id}
isKeySelected={
useSelectableList({
selectableListId: 'multiple-entity-select-list',
itemId: entity.id,
}).isSelectedItemId
}
selected={value[entity.id]}
onSelectChange={(newCheckedValue) =>
onChange({ ...value, [entity.id]: newCheckedValue })
}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
text={entity.name}
/>
}
text={entity.name}
/>
))}
</SelectableItem>
))}
</SelectableList>
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />}
</DropdownMenuItemsContainer>
</DropdownMenu>

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { Key } from 'ts-key-enum';
@ -6,6 +7,9 @@ import { IconPlus } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
@ -64,7 +68,7 @@ export const SingleEntitySelectBase = <
assertNotNull(entity) && isNonEmptyString(entity.name),
);
const { preselectedOptionId, resetScroll } = useEntitySelectScroll({
const { preselectedOptionId } = useEntitySelectScroll({
selectableOptionIds: [
EmptyButtonId,
...entitiesInDropdown.map((item) => item.id),
@ -73,24 +77,6 @@ export const SingleEntitySelectBase = <
containerRef,
});
useScopedHotkeys(
Key.Enter,
() => {
if (showCreateButton && preselectedOptionId === CreateButtonId) {
onCreate?.();
} else {
const entity = entitiesInDropdown.findIndex(
(entity) => entity.id === preselectedOptionId,
);
onEntitySelected(entitiesInDropdown[entity]);
}
resetScroll();
},
RelationPickerHotkeyScope.RelationPicker,
[entitiesInDropdown, preselectedOptionId, onEntitySelected],
);
useScopedHotkeys(
Key.Escape,
() => {
@ -100,6 +86,8 @@ export const SingleEntitySelectBase = <
[onCancel],
);
const selectableItemIds = entitiesInDropdown.map((entity) => entity.id);
return (
<div ref={containerRef}>
<DropdownMenuItemsContainer hasMaxHeight>
@ -130,23 +118,49 @@ export const SingleEntitySelectBase = <
/>
)}
{entitiesInDropdown?.map((entity) => (
<MenuItemSelectAvatar
key={entity.id}
testId="menu-item"
selected={selectedEntity?.id === entity.id}
onClick={() => onEntitySelected(entity)}
text={entity.name}
hovered={preselectedOptionId === entity.id}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
<SelectableList
selectableListId="single-entity-select-base-list"
selectableItemIds={[selectableItemIds]}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(_itemId) => {
if (
showCreateButton &&
preselectedOptionId === CreateButtonId
) {
onCreate?.();
} else {
const entity = entitiesInDropdown.findIndex(
(entity) => entity.id === _itemId,
);
onEntitySelected(entitiesInDropdown[entity]);
}
}}
>
<SelectableItem itemId={entity.id} key={entity.id}>
<MenuItemSelectAvatar
key={entity.id}
testId="menu-item"
onClick={() => onEntitySelected(entity)}
text={entity.name}
selected={selectedEntity?.id === entity.id}
hovered={
useSelectableList({
selectableListId: 'single-entity-select-base-list',
itemId: entity.id,
}).isSelectedItemId
}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
/>
}
/>
</SelectableItem>
</SelectableList>
))}
</>
)}

View File

@ -115,7 +115,6 @@ export const turnFiltersIntoObjectRecordFilters = (
);
}
// eslint-disable-next-line no-case-declarations
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
if (parsedRecordIds.length > 0) {