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

View File

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