Add the ability to limit the number of users you can select (#144618)

This commit is contained in:
Thom Heymann 2022-11-07 12:39:30 +00:00 committed by GitHub
parent 2e495074fd
commit a7976e57aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 22 deletions

View file

@ -38,6 +38,18 @@ export const AvatarDemo: FunctionComponent = () => {
 
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} />
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h3>Disabled</h3>
</EuiTitle>
<EuiSpacer size="s" />
<UserAvatar
user={userProfile.user}
avatar={{ ...userProfile.data.avatar, imageUrl: undefined }}
isDisabled
/>
&ensp;
<UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} isDisabled />
<EuiSpacer size="l" />
<EuiTitle size="xs">
<h3>Unknown</h3>
</EuiTitle>

View file

@ -110,6 +110,7 @@ export const PopoverDemo: FunctionComponent = () => {
selectedOptions,
defaultOptions,
onChange: setSelectedOptions,
limit: 2,
height: 32 * 8,
}}
panelStyle={{
@ -140,6 +141,7 @@ export const PopoverDemo: FunctionComponent = () => {
selectedOptions: selectedOptions2,
options: options2,
onChange: setSelectedOptions2,
limit: 2,
height: 32 * 8,
nullOptionLabel: 'Unassigned',
}}

View file

@ -62,6 +62,7 @@ export const SelectableDemo: FunctionComponent = () => {
selectedOptions={selectedOptions}
defaultOptions={defaultOptions}
onChange={setSelectedOptions}
limit={2}
/>
</PanelWithCodeBlock>
);

View file

@ -84,6 +84,37 @@ describe('UserProfilesSelectable', () => {
]);
});
it('should render warning and disable remaining users when limit has been reached', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(
<UserProfilesSelectable
selectedOptions={[firstOption]}
defaultOptions={[secondOption, thirdOption]}
limit={1}
/>
);
expect(wrapper.find('EuiCallOut').prop('color')).toEqual('warning');
expect(wrapper.find('EuiSelectable').prop('options')).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: firstOption.uid,
checked: 'on',
disabled: false,
}),
expect.objectContaining({
key: secondOption.uid,
checked: undefined,
disabled: true,
}),
expect.objectContaining({
key: thirdOption.uid,
checked: undefined,
disabled: true,
}),
])
);
});
it('should hide `selectedOptions` and `defaultOptions` when `options` has been provided', () => {
const [firstOption, secondOption, thirdOption] = userProfiles;
const wrapper = mount(

View file

@ -16,6 +16,7 @@ import {
EuiSelectable,
EuiSpacer,
EuiText,
EuiCallOut,
EuiHighlight,
EuiTextColor,
} from '@elastic/eui';
@ -59,6 +60,13 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
*/
options?: Option[];
/**
* Maximum number of users allowed to be selected.
*
* This limit is not enforced and only used to show a warning message.
*/
limit?: number;
/**
* Passes back the current selection.
* @param options Either the list of selected users or `null` (no users).
@ -87,11 +95,17 @@ export interface UserProfilesSelectableProps<Option extends UserProfileWithAvata
searchInputId?: string;
/**
* Returns text for selected status.
* Returns message for number of selected users.
* @param selectedCount Number of selected users
*/
selectedStatusMessage?(selectedCount: number): ReactNode;
/**
* Returns message when maximum number of selected users are reached.
* @param limit Maximum number of users allowed to be selected
*/
limitReachedMessage?(limit: number): ReactNode;
/**
* Label for clear button.
*/
@ -119,6 +133,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
onSearchChange,
isLoading = false,
singleSelection = false,
limit,
height,
loadingMessage,
noMatchesMessage,
@ -127,6 +142,7 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
searchPlaceholder,
searchInputId,
selectedStatusMessage,
limitReachedMessage,
nullOptionLabel,
defaultOptionsLabel,
clearButtonLabel,
@ -134,6 +150,9 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
const [displayedOptions, setDisplayedOptions] = useState<SelectableOption[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const selectedCount = selectedOptions ? selectedOptions.length : 0;
const limitReached = limit ? selectedCount >= limit : false;
// Resets all displayed options
const resetDisplayedOptions = () => {
if (options) {
@ -208,18 +227,30 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
values.map((option) => {
if (selectedOptions) {
const match = selectedOptions.find((profile) => isMatchingOption(option, profile));
return { ...option, checked: match === undefined ? undefined : 'on' };
const checked = match === undefined ? undefined : 'on';
const disabled = checked ? false : limitReached;
return {
...option,
checked,
disabled,
prepend: option.data ? (
<UserAvatar
user={option.data.user}
avatar={option.data.data?.avatar}
size="s"
isDisabled={disabled}
/>
) : undefined,
};
}
return { ...option, checked: undefined };
return { ...option, checked: undefined, disabled: undefined };
})
);
};
useEffect(resetDisplayedOptions, [options]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(updateDisplayedOptions, [defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]);
const selectedCount = selectedOptions ? selectedOptions.length : 0;
useEffect(updateCheckedStatus, [options, defaultOptions, selectedOptions]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<EuiSelectable
@ -298,12 +329,12 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
gutterSize="s"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiFlexItem>
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
</EuiFlexItem>
{option.user.email && option.user.email !== option.label ? (
<EuiFlexItem grow={false}>
<EuiTextColor color="subdued">
<EuiTextColor color={option.disabled ? 'disabled' : 'subdued'}>
{searchValue ? (
<EuiHighlight search={searchValue}>{option.user.email}</EuiHighlight>
) : (
@ -365,6 +396,26 @@ export const UserProfilesSelectable = <Option extends UserProfileWithAvatar | nu
</>
) : undefined}
</EuiPanel>
{limit && selectedCount >= limit ? (
<>
<EuiHorizontalRule margin="none" />
<EuiCallOut
title={
limitReachedMessage ? (
limitReachedMessage(limit)
) : (
<FormattedMessage
id="userProfileComponents.userProfilesSelectable.limitReachedMessage"
defaultMessage="You've selected the maximum of {count, plural, one {# user} other {# users}}"
values={{ count: limit }}
/>
)
}
color="warning"
size="s"
/>
</>
) : undefined}
<EuiHorizontalRule margin="none" />
{list}
</>
@ -382,7 +433,6 @@ function toSelectableOption(
if (userProfile) {
return {
key: userProfile.uid,
prepend: <UserAvatar user={userProfile.user} avatar={userProfile.data.avatar} size="s" />,
label: getUserDisplayName(userProfile.user),
data: userProfile,
};

View file

@ -6,13 +6,11 @@ exports[`<SpaceAwarePrivilegeSection> with user profile disabling "manageSpaces"
data-test-subj="userCannotManageSpacesCallout"
iconType="alert"
title={
<p>
<FormattedMessage
defaultMessage="Insufficient Privileges"
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
values={Object {}}
/>
</p>
<FormattedMessage
defaultMessage="Insufficient Privileges"
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
values={Object {}}
/>
}
>
<p>

View file

@ -82,12 +82,10 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
return (
<EuiCallOut
title={
<p>
<FormattedMessage
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
defaultMessage="Insufficient Privileges"
/>
</p>
<FormattedMessage
id="xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription"
defaultMessage="Insufficient Privileges"
/>
}
iconType="alert"
color="danger"