Fix hook bug (#2995)
* Fix hook bug * Fix --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -89,7 +89,7 @@ export const MultipleEntitySelect = <
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
selectableListId="multiple-entity-select-list"
|
||||
selectableItemIds={[selectableItemIds]}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||
onEnter={(_itemId) => {
|
||||
if (_itemId in value === false || value[_itemId] === false) {
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
|
||||
type SelectableMenuItemSelectProps = {
|
||||
entity: EntityForSelect;
|
||||
onEntitySelected: (entitySelected?: EntityForSelect) => void;
|
||||
selectedEntity?: EntityForSelect;
|
||||
};
|
||||
|
||||
const StyledSelectableItem = styled(SelectableItem)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SelectableMenuItemSelect = ({
|
||||
entity,
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
}: SelectableMenuItemSelectProps) => {
|
||||
const { isSelectedItemId } = useSelectableList({
|
||||
selectableListId: 'single-entity-select-base-list',
|
||||
itemId: entity.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledSelectableItem 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={isSelectedItemId}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={entity.avatarUrl}
|
||||
colorId={entity.id}
|
||||
placeholder={entity.name}
|
||||
size="md"
|
||||
type={entity.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
};
|
||||
@ -7,21 +7,18 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||
import { EntityForSelect } from '../types/EntityForSelect';
|
||||
|
||||
import {
|
||||
SingleEntitySelectBase,
|
||||
SingleEntitySelectBaseProps,
|
||||
} from './SingleEntitySelectBase';
|
||||
|
||||
export type SingleEntitySelectProps<
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
> = {
|
||||
export type SingleEntitySelectProps = {
|
||||
disableBackgroundBlur?: boolean;
|
||||
onCreate?: () => void;
|
||||
width?: number;
|
||||
} & Pick<
|
||||
SingleEntitySelectBaseProps<CustomEntityForSelect>,
|
||||
SingleEntitySelectBaseProps,
|
||||
| 'EmptyIcon'
|
||||
| 'emptyLabel'
|
||||
| 'entitiesToSelect'
|
||||
@ -31,9 +28,7 @@ export type SingleEntitySelectProps<
|
||||
| 'selectedEntity'
|
||||
>;
|
||||
|
||||
export const SingleEntitySelect = <
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
>({
|
||||
export const SingleEntitySelect = ({
|
||||
EmptyIcon,
|
||||
disableBackgroundBlur = false,
|
||||
emptyLabel,
|
||||
@ -44,7 +39,7 @@ export const SingleEntitySelect = <
|
||||
onEntitySelected,
|
||||
selectedEntity,
|
||||
width = 200,
|
||||
}: SingleEntitySelectProps<CustomEntityForSelect>) => {
|
||||
}: SingleEntitySelectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
|
||||
|
||||
@ -1,39 +1,31 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { useRef } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { SelectableMenuItemSelect } from '@/object-record/relation-picker/components/SelectableMenuItemSelect';
|
||||
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';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { CreateNewButton } from '../../../ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '../../../ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { CreateButtonId, EmptyButtonId } from '../constants';
|
||||
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
|
||||
import { EntityForSelect } from '../types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||
|
||||
export type SingleEntitySelectBaseProps<
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
> = {
|
||||
export type SingleEntitySelectBaseProps = {
|
||||
EmptyIcon?: IconComponent;
|
||||
emptyLabel?: string;
|
||||
entitiesToSelect: CustomEntityForSelect[];
|
||||
entitiesToSelect: EntityForSelect[];
|
||||
loading?: boolean;
|
||||
onCancel?: () => void;
|
||||
onEntitySelected: (entity?: CustomEntityForSelect) => void;
|
||||
selectedEntity?: CustomEntityForSelect;
|
||||
onEntitySelected: (entity?: EntityForSelect) => void;
|
||||
selectedEntity?: EntityForSelect;
|
||||
onCreate?: () => void;
|
||||
showCreateButton?: boolean;
|
||||
SelectAllIcon?: IconComponent;
|
||||
@ -43,9 +35,7 @@ export type SingleEntitySelectBaseProps<
|
||||
onAllEntitySelected?: () => void;
|
||||
};
|
||||
|
||||
export const SingleEntitySelectBase = <
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
>({
|
||||
export const SingleEntitySelectBase = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
entitiesToSelect,
|
||||
@ -60,23 +50,14 @@ export const SingleEntitySelectBase = <
|
||||
isAllEntitySelected,
|
||||
isAllEntitySelectShown,
|
||||
onAllEntitySelected,
|
||||
}: SingleEntitySelectBaseProps<CustomEntityForSelect>) => {
|
||||
}: SingleEntitySelectBaseProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
|
||||
(entity): entity is CustomEntityForSelect =>
|
||||
(entity): entity is EntityForSelect =>
|
||||
assertNotNull(entity) && isNonEmptyString(entity.name),
|
||||
);
|
||||
|
||||
const { preselectedOptionId } = useEntitySelectScroll({
|
||||
selectableOptionIds: [
|
||||
EmptyButtonId,
|
||||
...entitiesInDropdown.map((item) => item.id),
|
||||
...(showCreateButton ? [CreateButtonId] : []),
|
||||
],
|
||||
containerRef,
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
@ -90,94 +71,72 @@ export const SingleEntitySelectBase = <
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<SelectableList
|
||||
selectableListId="single-entity-select-base-list"
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||
onEnter={(_itemId) => {
|
||||
if (showCreateButton) {
|
||||
onCreate?.();
|
||||
} else {
|
||||
const entity = entitiesInDropdown.findIndex(
|
||||
(entity) => entity.id === _itemId,
|
||||
);
|
||||
onEntitySelected(entitiesInDropdown[entity]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<DropdownMenuSkeletonItem />
|
||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{isAllEntitySelectShown &&
|
||||
selectAllLabel &&
|
||||
onAllEntitySelected && (
|
||||
<MenuItemSelect
|
||||
key="select-all"
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
selected={!!isAllEntitySelected}
|
||||
/>
|
||||
)}
|
||||
{emptyLabel && (
|
||||
<MenuItemSelect
|
||||
onClick={() => onAllEntitySelected()}
|
||||
LeftIcon={SelectAllIcon}
|
||||
text={selectAllLabel}
|
||||
hovered={preselectedOptionId === EmptyButtonId}
|
||||
selected={!!isAllEntitySelected}
|
||||
key="select-none"
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={!selectedEntity}
|
||||
/>
|
||||
)}
|
||||
{emptyLabel && (
|
||||
<MenuItemSelect
|
||||
onClick={() => onEntitySelected()}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
hovered={preselectedOptionId === EmptyButtonId}
|
||||
selected={!selectedEntity}
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<SelectableMenuItemSelect
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onEntitySelected={onEntitySelected}
|
||||
selectedEntity={selectedEntity}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
{showCreateButton && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSeparator />
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
)}
|
||||
{entitiesInDropdown?.map((entity) => (
|
||||
<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>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
{showCreateButton && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSeparator />
|
||||
<CreateNewButton
|
||||
onClick={onCreate}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
hovered={preselectedOptionId === CreateButtonId}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
</SelectableList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
import scrollIntoView from 'scroll-into-view';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { CreateButtonId } from '../constants';
|
||||
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
|
||||
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
|
||||
|
||||
export const useEntitySelectScroll = ({
|
||||
containerRef,
|
||||
selectableOptionIds,
|
||||
}: {
|
||||
selectableOptionIds: string[];
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const { relationPickerPreselectedId, setRelationPickerPreselectedId } =
|
||||
useRelationPicker();
|
||||
|
||||
const preselectedIdIndex = getPreselectedIdIndex(
|
||||
selectableOptionIds,
|
||||
relationPickerPreselectedId ?? '',
|
||||
);
|
||||
|
||||
const resetScroll = () => {
|
||||
setRelationPickerPreselectedId('');
|
||||
|
||||
const preselectedRef = containerRef.current?.children[0] as HTMLElement;
|
||||
|
||||
scrollIntoView(preselectedRef, {
|
||||
align: {
|
||||
top: 0,
|
||||
},
|
||||
isScrollable: (target) => {
|
||||
return target === containerRef.current;
|
||||
},
|
||||
time: 0,
|
||||
});
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowUp,
|
||||
() => {
|
||||
const previousSelectableIndex = Math.max(preselectedIdIndex - 1, 0);
|
||||
const previousSelectableId = selectableOptionIds[previousSelectableIndex];
|
||||
setRelationPickerPreselectedId(previousSelectableId);
|
||||
const preselectedRef = containerRef.current?.children[
|
||||
previousSelectableIndex
|
||||
] as HTMLElement;
|
||||
|
||||
if (preselectedRef) {
|
||||
scrollIntoView(preselectedRef, {
|
||||
align: {
|
||||
top: 0.5,
|
||||
},
|
||||
isScrollable: (target) => {
|
||||
return target === containerRef.current;
|
||||
},
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
[selectableOptionIds],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowDown,
|
||||
() => {
|
||||
const nextSelectableIndex = Math.min(
|
||||
preselectedIdIndex + 1,
|
||||
selectableOptionIds?.length - 1,
|
||||
);
|
||||
const nextSelectableId = selectableOptionIds[nextSelectableIndex];
|
||||
setRelationPickerPreselectedId(nextSelectableId);
|
||||
if (nextSelectableId !== CreateButtonId) {
|
||||
const preselectedRef = containerRef.current?.children[
|
||||
nextSelectableIndex
|
||||
] as HTMLElement;
|
||||
|
||||
if (preselectedRef) {
|
||||
scrollIntoView(preselectedRef, {
|
||||
align: {
|
||||
top: 0.15,
|
||||
},
|
||||
isScrollable: (target) => target === containerRef.current,
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
[selectableOptionIds],
|
||||
);
|
||||
|
||||
return {
|
||||
preselectedOptionId: relationPickerPreselectedId,
|
||||
resetScroll,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user