1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

refactor: project users virtual autocomplete (#9196)

This commit is contained in:
Mateusz Kwasniewski 2025-02-04 10:04:36 +01:00 committed by GitHub
parent ef8191c68d
commit c68a542a63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 97 additions and 131 deletions

View File

@ -1,4 +1,4 @@
import { Autocomplete, Checkbox, styled, TextField } from '@mui/material';
import { Checkbox, styled, TextField } from '@mui/material';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import type { IUser } from 'interfaces/user';
@ -9,7 +9,7 @@ import { UG_USERS_ID } from 'utils/testIds';
import { caseInsensitiveSearch } from 'utils/search';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
import type { IServiceAccount } from 'interfaces/service-account';
import AutocompleteVirtual from './AutcompleteVirtual';
import AutocompleteVirtual from 'component/common/AutocompleteVirtual/AutcompleteVirtual';
const StyledOption = styled('div')(({ theme }) => ({
display: 'flex',
@ -110,91 +110,45 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
return (
<StyledGroupFormUsersSelect>
{isLargeList ? (
<AutocompleteVirtual
data-testid={UG_USERS_ID}
size='small'
limitTags={1}
openOnFocus
multiple
disableCloseOnSelect
value={users as UserOption[]}
onChange={(event, newValue, reason) => {
if (
event.type === 'keydown' &&
(event as React.KeyboardEvent).key ===
'Backspace' &&
reason === 'removeOption'
) {
return;
}
setUsers(newValue);
}}
options={options}
renderOption={renderOption}
filterOptions={(options, { inputValue }) =>
options.filter(
({ name, username, email }) =>
caseInsensitiveSearch(inputValue, email) ||
caseInsensitiveSearch(inputValue, name) ||
caseInsensitiveSearch(inputValue, username),
)
<AutocompleteVirtual
data-testid={UG_USERS_ID}
size='small'
limitTags={1}
openOnFocus
multiple
disableCloseOnSelect
value={users as UserOption[]}
onChange={(event, newValue, reason) => {
if (
event.type === 'keydown' &&
(event as React.KeyboardEvent).key === 'Backspace' &&
reason === 'removeOption'
) {
return;
}
isOptionEqualToValue={(option, value) =>
option.id === value.id
}
getOptionLabel={(option: UserOption) =>
option.email || option.name || option.username || ''
}
renderInput={(params) => (
<TextField {...params} label='Select users' />
)}
renderTags={(value) => renderTags(value)}
/>
) : (
<Autocomplete
data-testid={UG_USERS_ID}
size='small'
limitTags={1}
openOnFocus
multiple
disableCloseOnSelect
value={users as UserOption[]}
onChange={(event, newValue, reason) => {
if (
event.type === 'keydown' &&
(event as React.KeyboardEvent).key ===
'Backspace' &&
reason === 'removeOption'
) {
return;
}
setUsers(newValue);
}}
groupBy={(option) => option.type}
options={options}
renderOption={renderOption}
filterOptions={(options, { inputValue }) =>
options.filter(
({ name, username, email }) =>
caseInsensitiveSearch(inputValue, email) ||
caseInsensitiveSearch(inputValue, name) ||
caseInsensitiveSearch(inputValue, username),
)
}
isOptionEqualToValue={(option, value) =>
option.id === value.id
}
getOptionLabel={(option: UserOption) =>
option.email || option.name || option.username || ''
}
renderInput={(params) => (
<TextField {...params} label='Select users' />
)}
renderTags={(value) => renderTags(value)}
noOptionsText={isLoading ? 'Loading…' : 'No options'}
/>
)}
setUsers(newValue);
}}
options={options}
groupBy={(option) => option.type}
renderOption={renderOption}
filterOptions={(options, { inputValue }) =>
options.filter(
({ name, username, email }) =>
caseInsensitiveSearch(inputValue, email) ||
caseInsensitiveSearch(inputValue, name) ||
caseInsensitiveSearch(inputValue, username),
)
}
isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option: UserOption) =>
option.email || option.name || option.username || ''
}
renderInput={(params) => (
<TextField {...params} label='Select users' />
)}
renderTags={(value) => renderTags(value)}
noOptionsText={isLoading ? 'Loading…' : 'No options'}
/>
</StyledGroupFormUsersSelect>
);
};

View File

@ -61,24 +61,38 @@ const ListboxComponent = forwardRef<
);
});
type TProps<T, M extends boolean | undefined> = Omit<
type AutocompleteVirtualProps<T, M extends boolean | undefined> = Omit<
AutocompleteProps<T, M, boolean, false>,
'autoHighlight' | 'disableListWrap' | 'ListboxComponent' | 'groupBy'
>;
'disableListWrap' | 'ListboxComponent'
> & {
virtualThreshold?: number;
};
// This component has a default threshold of 250 when virtualization kicks in
// When virtualization is enabled we skip groupBy
function AutocompleteVirtual<T, M extends boolean | undefined>(
props: TProps<T, M>,
props: AutocompleteVirtualProps<T, M>,
) {
const { getOptionLabel, className, ...restAutocompleteProps } = props;
const {
virtualThreshold = 250,
getOptionLabel,
className,
...restAutocompleteProps
} = props;
return (
<Autocomplete
{...restAutocompleteProps}
disableListWrap
getOptionLabel={getOptionLabel}
ListboxComponent={ListboxComponent}
/>
);
const isLargeList = props.options.length > virtualThreshold;
const autocompleteProps = {
...restAutocompleteProps,
getOptionLabel,
disableListWrap: true,
...(isLargeList && {
ListboxComponent: ListboxComponent,
groupBy: undefined,
}),
};
return <Autocomplete {...autocompleteProps} />;
}
export default AutocompleteVirtual;

View File

@ -1,7 +1,6 @@
import type React from 'react';
import { type FormEvent, useState } from 'react';
import {
Autocomplete,
Button,
capitalize,
Checkbox,
@ -40,6 +39,7 @@ import { MultipleRoleSelect } from 'component/common/MultipleRoleSelect/Multiple
import type { IUserProjectRole } from '../../../../interfaces/userProjectRoles';
import { useCheckProjectPermissions } from 'hooks/useHasAccess';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import AutocompleteVirtual from 'component/common/AutocompleteVirtual/AutcompleteVirtual';
const StyledForm = styled('form')(() => ({
display: 'flex',
@ -339,6 +339,7 @@ export const ProjectAccessAssign = ({
userRoles.some((userrole) => role.id === userrole.id),
);
}
return (
<SidebarModal
open
@ -362,7 +363,7 @@ export const ProjectAccessAssign = ({
Select the {entityType}
</StyledInputDescription>
<StyledAutocompleteWrapper>
<Autocomplete
<AutocompleteVirtual
data-testid={PA_USERS_GROUPS_ID}
size='small'
multiple
@ -414,37 +415,34 @@ export const ProjectAccessAssign = ({
}
}}
filterOptions={(options, { inputValue }) =>
options
.filter((option: IAccessOption) => {
if (
option.type ===
ENTITY_TYPE.USER ||
option.type ===
ENTITY_TYPE.SERVICE_ACCOUNT
) {
const optionUser =
option.entity as IUser;
return (
caseInsensitiveSearch(
inputValue,
optionUser.email,
) ||
caseInsensitiveSearch(
inputValue,
optionUser.name,
) ||
caseInsensitiveSearch(
inputValue,
optionUser.username,
)
);
}
return caseInsensitiveSearch(
inputValue,
option.entity.name,
options.filter((option: IAccessOption) => {
if (
option.type === ENTITY_TYPE.USER ||
option.type ===
ENTITY_TYPE.SERVICE_ACCOUNT
) {
const optionUser =
option.entity as IUser;
return (
caseInsensitiveSearch(
inputValue,
optionUser.email,
) ||
caseInsensitiveSearch(
inputValue,
optionUser.name,
) ||
caseInsensitiveSearch(
inputValue,
optionUser.username,
)
);
})
.slice(0, 100)
}
return caseInsensitiveSearch(
inputValue,
option.entity.name,
);
})
}
isOptionEqualToValue={(option, value) =>
option.type === value.type &&