mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add no assignees option to user profiles selectable (#140036)
* Add no assignees option to user profiles selectable * Allow simultaneous selection of no users option and user profiles * Update filters demo * Fixing tests because of it getting split into components * Added suggestions from code review Co-authored-by: Jonathan Buttner <jonathan.buttner@elastic.co>
This commit is contained in:
parent
12acc999c0
commit
40b1a67ae5
6 changed files with 313 additions and 93 deletions
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFilterGroup, EuiFilterButton, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { UserProfilesPopover, UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
import { PanelWithCodeBlock } from './panel_with_code_block';
|
||||
|
||||
|
@ -57,6 +57,44 @@ export const PopoverDemo: FunctionComponent = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const [isOpen2, setIsOpen2] = useState(false);
|
||||
const [selectedOptions2, setSelectedOptions2] = useState<Array<UserProfileWithAvatar | null>>([
|
||||
null,
|
||||
]);
|
||||
const options2: Array<UserProfileWithAvatar | null> = [
|
||||
null,
|
||||
{
|
||||
uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0',
|
||||
enabled: true,
|
||||
data: {},
|
||||
user: {
|
||||
username: 'damaged_raccoon',
|
||||
email: 'damaged_raccoon@elastic.co',
|
||||
full_name: 'Damaged Raccoon',
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: 'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
|
||||
enabled: true,
|
||||
data: {},
|
||||
user: {
|
||||
username: 'physical_dinosaur',
|
||||
email: 'physical_dinosaur@elastic.co',
|
||||
full_name: 'Physical Dinosaur',
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: 'u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0',
|
||||
enabled: true,
|
||||
data: {},
|
||||
user: {
|
||||
username: 'wet_dingo',
|
||||
email: 'wet_dingo@elastic.co',
|
||||
full_name: 'Wet Dingo',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PanelWithCodeBlock title="Popover" code={code}>
|
||||
<UserProfilesPopover
|
||||
|
@ -78,6 +116,38 @@ export const PopoverDemo: FunctionComponent = () => {
|
|||
width: 32 * 16,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h3>Unassigned option</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFilterGroup>
|
||||
<UserProfilesPopover
|
||||
button={
|
||||
<EuiFilterButton
|
||||
iconType="arrowDown"
|
||||
numFilters={options2.length}
|
||||
hasActiveFilters={selectedOptions2.length > 0}
|
||||
numActiveFilters={selectedOptions2.length}
|
||||
onClick={() => setIsOpen2((value) => !value)}
|
||||
>
|
||||
Assignees
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isOpen2}
|
||||
closePopover={() => setIsOpen2(false)}
|
||||
selectableProps={{
|
||||
selectedOptions: selectedOptions2,
|
||||
options: options2,
|
||||
onChange: setSelectedOptions2,
|
||||
height: 32 * 8,
|
||||
nullOptionLabel: 'Unassigned',
|
||||
}}
|
||||
panelStyle={{
|
||||
width: 32 * 16,
|
||||
}}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
</PanelWithCodeBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
*/
|
||||
|
||||
import type { EuiPopoverProps, EuiContextMenuPanelProps } from '@elastic/eui';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiPopover, EuiContextMenuPanel, useGeneratedHtmlId } from '@elastic/eui';
|
||||
|
||||
import { UserProfilesSelectable, UserProfilesSelectableProps } from './user_profiles_selectable';
|
||||
import type { UserProfileWithAvatar } from './user_avatar';
|
||||
|
||||
/**
|
||||
* Props of {@link UserProfilesPopover} component
|
||||
*/
|
||||
export interface UserProfilesPopoverProps extends EuiPopoverProps {
|
||||
export interface UserProfilesPopoverProps<Option extends UserProfileWithAvatar | null>
|
||||
extends EuiPopoverProps {
|
||||
/**
|
||||
* Title of the popover
|
||||
* @see EuiContextMenuPanelProps
|
||||
|
@ -27,17 +28,17 @@ export interface UserProfilesPopoverProps extends EuiPopoverProps {
|
|||
* Props forwarded to selectable component
|
||||
* @see UserProfilesSelectableProps
|
||||
*/
|
||||
selectableProps: UserProfilesSelectableProps;
|
||||
selectableProps: UserProfilesSelectableProps<Option>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a selectable component inside a popover given a list of user profiles
|
||||
*/
|
||||
export const UserProfilesPopover: FunctionComponent<UserProfilesPopoverProps> = ({
|
||||
export const UserProfilesPopover = <Option extends UserProfileWithAvatar | null>({
|
||||
title,
|
||||
selectableProps,
|
||||
...popoverProps
|
||||
}) => {
|
||||
}: UserProfilesPopoverProps<Option>) => {
|
||||
const searchInputId = useGeneratedHtmlId({
|
||||
prefix: 'searchInput',
|
||||
conditionalId: selectableProps.searchInputId,
|
||||
|
|
|
@ -199,11 +199,73 @@ describe('UserProfilesSelectable', () => {
|
|||
const onSearchChange = jest.fn();
|
||||
const wrapper = mount(<UserProfilesSelectable onSearchChange={onSearchChange} />);
|
||||
wrapper.find('input[type="search"]').simulate('change', { target: { value: 'search' } });
|
||||
expect(onSearchChange).toHaveBeenCalledWith('search', []);
|
||||
expect(onSearchChange).toHaveBeenCalledWith('search');
|
||||
});
|
||||
|
||||
it('should set `id` prop of search input correctly', () => {
|
||||
const wrapper = mount(<UserProfilesSelectable searchInputId="testSearchField" />);
|
||||
expect(wrapper.find('input[type="search"]').prop('id')).toBe('testSearchField');
|
||||
});
|
||||
|
||||
describe('with "no users" option', () => {
|
||||
it('should render `null` option correctly', () => {
|
||||
const [firstOption] = userProfiles;
|
||||
const wrapper = mount(
|
||||
<UserProfilesSelectable selectedOptions={[null]} options={[null, firstOption]} />
|
||||
);
|
||||
expect(wrapper.find('EuiSelectable').prop('options')).toEqual([
|
||||
expect.objectContaining({
|
||||
key: 'null',
|
||||
checked: 'on',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: firstOption.uid,
|
||||
checked: undefined,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should trigger `onChange` callback with `null` when "no users" get selected', () => {
|
||||
const onChange = jest.fn();
|
||||
const [firstOption] = userProfiles;
|
||||
const wrapper = mount(
|
||||
<UserProfilesSelectable options={[null, firstOption]} onChange={onChange} />
|
||||
);
|
||||
|
||||
wrapper.find('EuiSelectableListItem').first().simulate('click');
|
||||
expect(onChange).toHaveBeenCalledWith(expect.arrayContaining([null]));
|
||||
});
|
||||
|
||||
it('should trigger `onChange` callback with empty array when nothing gets selected', () => {
|
||||
const onChange = jest.fn();
|
||||
const [firstOption] = userProfiles;
|
||||
const wrapper = mount(
|
||||
<UserProfilesSelectable
|
||||
selectedOptions={[null]}
|
||||
options={[null, firstOption]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('EuiSelectableListItem').first().simulate('click');
|
||||
expect(onChange).toHaveBeenCalledWith(expect.arrayContaining([]));
|
||||
});
|
||||
|
||||
it('should trigger `onChange` callback with selected option when selected', () => {
|
||||
const onChange = jest.fn();
|
||||
const [firstOption] = userProfiles;
|
||||
const wrapper = mount(
|
||||
<UserProfilesSelectable options={[null, firstOption]} onChange={onChange} />
|
||||
);
|
||||
|
||||
wrapper.find('EuiSelectableListItem').last().simulate('click');
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
uid: firstOption.uid,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,9 +16,10 @@ import {
|
|||
EuiSelectable,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiHighlight,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import type { FunctionComponent, ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -28,10 +29,12 @@ import { getUserDisplayName } from './user_profile';
|
|||
import type { UserProfileWithAvatar } from './user_avatar';
|
||||
import { UserAvatar } from './user_avatar';
|
||||
|
||||
const NULL_OPTION_KEY = 'null';
|
||||
|
||||
/**
|
||||
* Props of {@link UserProfilesSelectable} component
|
||||
*/
|
||||
export interface UserProfilesSelectableProps
|
||||
export interface UserProfilesSelectableProps<Option extends UserProfileWithAvatar | null>
|
||||
extends Pick<
|
||||
EuiSelectableProps,
|
||||
| 'height'
|
||||
|
@ -44,23 +47,23 @@ export interface UserProfilesSelectableProps
|
|||
/**
|
||||
* List of users to be rendered as suggestions.
|
||||
*/
|
||||
defaultOptions?: UserProfileWithAvatar[];
|
||||
defaultOptions?: Option[];
|
||||
|
||||
/**
|
||||
* List of selected users.
|
||||
* List of selected users or `null` (no users).
|
||||
*/
|
||||
selectedOptions?: UserProfileWithAvatar[];
|
||||
selectedOptions?: Option[];
|
||||
|
||||
/**
|
||||
* List of users from search results. Should be updated based on the search term provided by `onSearchChange` callback.
|
||||
*/
|
||||
options?: UserProfileWithAvatar[];
|
||||
options?: Option[];
|
||||
|
||||
/**
|
||||
* Passes back the list of selected users.
|
||||
* @param options List of selected users
|
||||
* Passes back the current selection.
|
||||
* @param options Either the list of selected users or `null` (no users).
|
||||
*/
|
||||
onChange?(options: UserProfileWithAvatar[]): void;
|
||||
onChange?(options: Option[]): void;
|
||||
|
||||
/**
|
||||
* Passes back the search term.
|
||||
|
@ -90,15 +93,25 @@ export interface UserProfilesSelectableProps
|
|||
selectedStatusMessage?(selectedCount: number): ReactNode;
|
||||
|
||||
/**
|
||||
* Text for label of clear button.
|
||||
* Label for clear button.
|
||||
*/
|
||||
clearButtonLabel?: ReactNode;
|
||||
|
||||
/**
|
||||
* Label of "no users" option.
|
||||
*/
|
||||
nullOptionLabel?: string;
|
||||
|
||||
/**
|
||||
* Label for default options group separator.
|
||||
*/
|
||||
defaultOptionsLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a selectable component given a list of user profiles
|
||||
*/
|
||||
export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectableProps> = ({
|
||||
export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | null>({
|
||||
selectedOptions,
|
||||
defaultOptions,
|
||||
options,
|
||||
|
@ -114,14 +127,17 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
searchPlaceholder,
|
||||
searchInputId,
|
||||
selectedStatusMessage,
|
||||
nullOptionLabel,
|
||||
defaultOptionsLabel,
|
||||
clearButtonLabel,
|
||||
}) => {
|
||||
}: UserProfilesSelectableProps<Option>) => {
|
||||
const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Resets all displayed options
|
||||
const resetDisplayedOptions = () => {
|
||||
if (options) {
|
||||
setDisplayedOptions(options.map(toSelectableOption));
|
||||
setDisplayedOptions(options.map((option) => toSelectableOption(option, nullOptionLabel)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -133,11 +149,13 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
let index = values.findIndex((option) => option.isGroupLabel);
|
||||
if (index === -1) {
|
||||
const length = values.push({
|
||||
label: i18n.translate('userProfileComponents.userProfilesSelectable.suggestedLabel', {
|
||||
defaultMessage: 'Suggested',
|
||||
}),
|
||||
label:
|
||||
defaultOptionsLabel ??
|
||||
i18n.translate('userProfileComponents.userProfilesSelectable.defaultOptionsLabel', {
|
||||
defaultMessage: 'Suggested',
|
||||
}),
|
||||
isGroupLabel: true,
|
||||
} as SelectableOption);
|
||||
});
|
||||
index = length - 1;
|
||||
}
|
||||
return index;
|
||||
|
@ -156,8 +174,8 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
// Get any newly added selected options
|
||||
const selectedOptionsToAdd: SelectableOption[] = selectedOptions
|
||||
? selectedOptions
|
||||
.filter((profile) => !nextOptions.find((option) => option.key === profile.uid))
|
||||
.map(toSelectableOption)
|
||||
.filter((profile) => !nextOptions.find((option) => isMatchingOption(option, profile)))
|
||||
.map((option) => toSelectableOption(option, nullOptionLabel))
|
||||
: [];
|
||||
|
||||
// Get any newly added default options
|
||||
|
@ -165,10 +183,10 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
? defaultOptions
|
||||
.filter(
|
||||
(profile) =>
|
||||
!nextOptions.find((option) => option.key === profile.uid) &&
|
||||
!selectedOptionsToAdd.find((option) => option.key === profile.uid)
|
||||
!nextOptions.find((option) => isMatchingOption(option, profile)) &&
|
||||
!selectedOptionsToAdd.find((option) => isMatchingOption(option, profile))
|
||||
)
|
||||
.map(toSelectableOption)
|
||||
.map((option) => toSelectableOption(option, nullOptionLabel))
|
||||
: [];
|
||||
|
||||
// Merge in any new options and add group separator if necessary
|
||||
|
@ -189,8 +207,8 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
setDisplayedOptions((values) =>
|
||||
values.map((option) => {
|
||||
if (selectedOptions) {
|
||||
const match = selectedOptions.find((p) => p.uid === option.key);
|
||||
return { ...option, checked: match ? 'on' : undefined };
|
||||
const match = selectedOptions.find((profile) => isMatchingOption(option, profile));
|
||||
return { ...option, checked: match === undefined ? undefined : 'on' };
|
||||
}
|
||||
return { ...option, checked: undefined };
|
||||
})
|
||||
|
@ -207,30 +225,40 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
<EuiSelectable
|
||||
options={displayedOptions}
|
||||
// @ts-expect-error: Type of `nextOptions` in EuiSelectable does not match what's actually being passed back so need to manually override it
|
||||
onChange={(nextOptions: Array<EuiSelectableOption<{ data: UserProfileWithAvatar }>>) => {
|
||||
onChange={(
|
||||
nextOptions: Array<EuiSelectableOption<{ data: Partial<UserProfileWithAvatar> }>>
|
||||
) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Take all selected options from `nextOptions` unless already in `props.selectedOptions`
|
||||
const values: UserProfileWithAvatar[] = nextOptions
|
||||
// @ts-expect-error
|
||||
const values: Option[] = nextOptions
|
||||
.filter((option) => {
|
||||
if (option.isGroupLabel || option.checked !== 'on') {
|
||||
return false;
|
||||
}
|
||||
if (selectedOptions && selectedOptions.find((p) => p.uid === option.key)) {
|
||||
if (
|
||||
selectedOptions &&
|
||||
selectedOptions.find((profile) => isMatchingOption(option, profile)) !== undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((option) => option.data);
|
||||
.map((option) => (option.key === NULL_OPTION_KEY ? null : option.data));
|
||||
|
||||
// Add all options from `props.selectedOptions` unless they have been deselected in `nextOptions`
|
||||
if (selectedOptions && !singleSelection) {
|
||||
selectedOptions.forEach((profile) => {
|
||||
const match = nextOptions.find((o) => o.key === profile.uid);
|
||||
if (!match || match.checked === 'on') {
|
||||
values.push(profile);
|
||||
const match = nextOptions.find((option) => isMatchingOption(option, profile));
|
||||
if (match === undefined || match.checked === 'on') {
|
||||
if (match && match.key === NULL_OPTION_KEY) {
|
||||
values.unshift(profile);
|
||||
} else {
|
||||
values.push(profile);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -246,7 +274,11 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
i18n.translate('userProfileComponents.userProfilesSelectable.searchPlaceholder', {
|
||||
defaultMessage: 'Search',
|
||||
}),
|
||||
onChange: onSearchChange,
|
||||
value: searchTerm,
|
||||
onChange: (value) => {
|
||||
setSearchTerm(value);
|
||||
onSearchChange?.(value);
|
||||
},
|
||||
isLoading,
|
||||
isClearable: !isLoading,
|
||||
id: searchInputId,
|
||||
|
@ -257,44 +289,81 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
noMatchesMessage={noMatchesMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
errorMessage={errorMessage}
|
||||
renderOption={(option, searchValue) => {
|
||||
if (option.user) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
{option.user.email ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTextColor color="subdued">
|
||||
{searchValue ? (
|
||||
<EuiHighlight search={searchValue}>{option.user.email}</EuiHighlight>
|
||||
) : (
|
||||
option.user.email
|
||||
)}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
) : undefined}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return <EuiHighlight search={searchValue}>{option.label}</EuiHighlight>;
|
||||
}}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} paddingSize="s">
|
||||
{search}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{selectedStatusMessage ? (
|
||||
selectedStatusMessage(selectedCount)
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="userProfileComponents.userProfilesSelectable.selectedStatusMessage"
|
||||
defaultMessage="{count, plural, one {# user selected} other {# users selected}}"
|
||||
values={{ count: selectedCount }}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{selectedCount ? (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="right"
|
||||
onClick={() => onChange?.([])}
|
||||
style={{ height: '1rem' }}
|
||||
>
|
||||
{clearButtonLabel ?? (
|
||||
<FormattedMessage
|
||||
id="userProfileComponents.userProfilesSelectable.clearButtonLabel"
|
||||
defaultMessage="Remove all users"
|
||||
/>
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
) : undefined}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!singleSelection ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{selectedStatusMessage ? (
|
||||
selectedStatusMessage(selectedCount)
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="userProfileComponents.userProfilesSelectable.selectedStatusMessage"
|
||||
defaultMessage="{count, plural, one {# user selected} other {# users selected}}"
|
||||
values={{ count: selectedCount }}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{selectedCount ? (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
flush="right"
|
||||
onClick={() => onChange?.([])}
|
||||
style={{ height: '1rem' }}
|
||||
>
|
||||
{clearButtonLabel ?? (
|
||||
<FormattedMessage
|
||||
id="userProfileComponents.userProfilesSelectable.clearButtonLabel"
|
||||
defaultMessage="Remove all users"
|
||||
/>
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
) : undefined}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : undefined}
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{list}
|
||||
|
@ -304,15 +373,33 @@ export const UserProfilesSelectable: FunctionComponent<UserProfilesSelectablePro
|
|||
);
|
||||
};
|
||||
|
||||
type SelectableOption = EuiSelectableOption<UserProfileWithAvatar>;
|
||||
type SelectableOption = EuiSelectableOption<Partial<UserProfileWithAvatar>>;
|
||||
|
||||
function toSelectableOption(userProfile: UserProfileWithAvatar): SelectableOption {
|
||||
// @ts-ignore: `isGroupLabel` is not required here but TS complains
|
||||
function toSelectableOption(
|
||||
userProfile: UserProfileWithAvatar | null,
|
||||
nullOptionLabel?: string
|
||||
): SelectableOption {
|
||||
if (userProfile) {
|
||||
return {
|
||||
key: userProfile.uid,
|
||||
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
|
||||
label: getUserDisplayName(userProfile.user),
|
||||
data: userProfile,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: userProfile.uid,
|
||||
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
|
||||
label: getUserDisplayName(userProfile.user),
|
||||
append: <EuiTextColor color="subdued">{userProfile.user.email}</EuiTextColor>,
|
||||
data: userProfile,
|
||||
key: NULL_OPTION_KEY,
|
||||
label:
|
||||
nullOptionLabel ??
|
||||
i18n.translate('userProfileComponents.userProfilesSelectable.nullOptionLabel', {
|
||||
defaultMessage: 'No users',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function isMatchingOption<Option extends UserProfileWithAvatar | null>(
|
||||
option: SelectableOption,
|
||||
profile: Option
|
||||
) {
|
||||
return option.key === (profile ? profile.uid : NULL_OPTION_KEY);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('AssigneesFilterPopover', () => {
|
|||
await waitForEuiPopoverOpen();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
|
||||
userEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
userEvent.click(screen.getByText('WD'));
|
||||
|
||||
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -75,7 +75,7 @@ describe('AssigneesFilterPopover', () => {
|
|||
await waitForEuiPopoverOpen();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
|
||||
userEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
userEvent.click(screen.getByText('WD'));
|
||||
userEvent.click(screen.getByText('damaged_raccoon@elastic.co'));
|
||||
|
||||
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
|
|
|
@ -44,10 +44,10 @@ describe('SuggestUsersPopover', () => {
|
|||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument();
|
||||
expect(screen.getByText('WD')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
fireEvent.click(screen.getByText('WD'));
|
||||
|
||||
expect(onUsersChange.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -75,12 +75,12 @@ describe('SuggestUsersPopover', () => {
|
|||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'elastic' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument();
|
||||
expect(screen.getByText('damaged_raccoon@elastic.co')).toBeInTheDocument();
|
||||
expect(screen.getByText('WD')).toBeInTheDocument();
|
||||
expect(screen.getByText('DR')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
fireEvent.click(screen.getByText('damaged_raccoon@elastic.co'));
|
||||
fireEvent.click(screen.getByText('WD'));
|
||||
fireEvent.click(screen.getByText('DR'));
|
||||
|
||||
expect(onUsersChange.mock.calls[1][0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -123,10 +123,10 @@ describe('SuggestUsersPopover', () => {
|
|||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'elastic' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument();
|
||||
expect(screen.getByText('WD')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
fireEvent.click(screen.getByText('WD'));
|
||||
|
||||
expect(onUsersChange.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -173,10 +173,10 @@ describe('SuggestUsersPopover', () => {
|
|||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('wet_dingo@elastic.co')).toBeInTheDocument();
|
||||
expect(screen.getByText('WD')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
fireEvent.click(screen.getByText('WD'));
|
||||
expect(screen.getByText('1 assigned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -187,7 +187,7 @@ describe('SuggestUsersPopover', () => {
|
|||
|
||||
expect(screen.queryByText('assigned')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
|
||||
fireEvent.click(screen.getByText('wet_dingo@elastic.co'));
|
||||
fireEvent.click(screen.getByText('WD'));
|
||||
expect(screen.getByText('1 assigned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue