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,7 +110,6 @@ 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'
@ -122,8 +121,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
onChange={(event, newValue, reason) => { onChange={(event, newValue, reason) => {
if ( if (
event.type === 'keydown' && event.type === 'keydown' &&
(event as React.KeyboardEvent).key === (event as React.KeyboardEvent).key === 'Backspace' &&
'Backspace' &&
reason === 'removeOption' reason === 'removeOption'
) { ) {
return; return;
@ -131,48 +129,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
setUsers(newValue); setUsers(newValue);
}} }}
options={options} 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)}
/>
) : (
<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} groupBy={(option) => option.type}
options={options}
renderOption={renderOption} renderOption={renderOption}
filterOptions={(options, { inputValue }) => filterOptions={(options, { inputValue }) =>
options.filter( options.filter(
@ -182,9 +139,7 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
caseInsensitiveSearch(inputValue, username), caseInsensitiveSearch(inputValue, username),
) )
} }
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) => option.id === value.id}
option.id === value.id
}
getOptionLabel={(option: UserOption) => getOptionLabel={(option: UserOption) =>
option.email || option.name || option.username || '' option.email || option.name || option.username || ''
} }
@ -194,7 +149,6 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
renderTags={(value) => renderTags(value)} renderTags={(value) => renderTags(value)}
noOptionsText={isLoading ? 'Loading…' : 'No options'} 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,11 +415,9 @@ export const ProjectAccessAssign = ({
} }
}} }}
filterOptions={(options, { inputValue }) => filterOptions={(options, { inputValue }) =>
options options.filter((option: IAccessOption) => {
.filter((option: IAccessOption) => {
if ( if (
option.type === option.type === ENTITY_TYPE.USER ||
ENTITY_TYPE.USER ||
option.type === option.type ===
ENTITY_TYPE.SERVICE_ACCOUNT ENTITY_TYPE.SERVICE_ACCOUNT
) { ) {
@ -444,7 +443,6 @@ export const ProjectAccessAssign = ({
option.entity.name, option.entity.name,
); );
}) })
.slice(0, 100)
} }
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) =>
option.type === value.type && option.type === value.type &&