mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
feat: virtual autocomplete (#9181)
This commit is contained in:
parent
6f19ce090d
commit
c86ea091b7
@ -46,6 +46,7 @@
|
||||
"@mui/material": "5.15.3",
|
||||
"@mui/x-date-pickers": "^7.0.0",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tanstack/react-virtual": "^3.11.3",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.1.0",
|
||||
|
@ -0,0 +1,84 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import {
|
||||
Children,
|
||||
type HTMLAttributes,
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Autocomplete, type AutocompleteProps } from '@mui/material';
|
||||
|
||||
const ListboxComponent = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLElement>
|
||||
>(function ListboxComponent(props, ref) {
|
||||
const { children, ...other } = props;
|
||||
const parentRef = useRef(null);
|
||||
const items = Children.toArray(children);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: Children.count(children),
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 56,
|
||||
overscan: 3,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={parentRef} {...other}>
|
||||
<ul
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const element = items[virtualRow.index] as ReactElement;
|
||||
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inlineStyle = {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
top: `${virtualRow.start}px`,
|
||||
};
|
||||
|
||||
return cloneElement(element, {
|
||||
ref: rowVirtualizer.measureElement,
|
||||
key: virtualRow.key,
|
||||
'data-index': virtualRow.index,
|
||||
style: inlineStyle,
|
||||
});
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
type TProps<T, M extends boolean | undefined> = Omit<
|
||||
AutocompleteProps<T, M, boolean, false>,
|
||||
'autoHighlight' | 'disableListWrap' | 'ListboxComponent' | 'groupBy'
|
||||
>;
|
||||
|
||||
function AutocompleteVirtual<T, M extends boolean | undefined>(
|
||||
props: TProps<T, M>,
|
||||
) {
|
||||
const { getOptionLabel, className, ...restAutocompleteProps } = props;
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
{...restAutocompleteProps}
|
||||
disableListWrap
|
||||
getOptionLabel={getOptionLabel}
|
||||
ListboxComponent={ListboxComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AutocompleteVirtual;
|
@ -9,6 +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';
|
||||
|
||||
const StyledOption = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -32,12 +33,14 @@ const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StrechedLi = styled('li')({ width: '100%' });
|
||||
|
||||
const renderOption = (
|
||||
props: React.HTMLAttributes<HTMLLIElement>,
|
||||
option: IUser,
|
||||
selected: boolean,
|
||||
{ selected }: { selected: boolean },
|
||||
) => (
|
||||
<li {...props}>
|
||||
<StrechedLi {...props} key={option.id}>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
|
||||
checkedIcon={<CheckBoxIcon fontSize='small' />}
|
||||
@ -52,7 +55,7 @@ const renderOption = (
|
||||
: option.email}
|
||||
</span>
|
||||
</StyledOption>
|
||||
</li>
|
||||
</StrechedLi>
|
||||
);
|
||||
|
||||
const renderTags = (value: IGroupUser[]) => (
|
||||
@ -76,73 +79,122 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
||||
users,
|
||||
setUsers,
|
||||
}) => {
|
||||
const { users: usersAll } = useUsers();
|
||||
const { serviceAccounts } = useServiceAccounts();
|
||||
const { users: usersAll, loading: isUsersLoading } = useUsers();
|
||||
const { serviceAccounts, loading: isServiceAccountsLoading } =
|
||||
useServiceAccounts();
|
||||
|
||||
const options = [
|
||||
...usersAll
|
||||
.map((user: IUser) => ({ ...user, type: 'USERS' }))
|
||||
.sort((a: IUser, b: IUser) => {
|
||||
const aName = a.name || a.username || '';
|
||||
const bName = b.name || b.username || '';
|
||||
return aName.localeCompare(bName);
|
||||
}),
|
||||
...serviceAccounts
|
||||
.map((serviceAccount: IServiceAccount) => ({
|
||||
...serviceAccount,
|
||||
type: 'SERVICE ACCOUNTS',
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const aName = a.name || a.username || '';
|
||||
const bName = b.name || b.username || '';
|
||||
return aName.localeCompare(bName);
|
||||
}),
|
||||
];
|
||||
const isLoading = isUsersLoading || isServiceAccountsLoading;
|
||||
const options = isLoading
|
||||
? []
|
||||
: [
|
||||
...usersAll
|
||||
.map((user: IUser) => ({ ...user, type: 'USERS' }))
|
||||
.sort((a: IUser, b: IUser) => {
|
||||
const aName = a.name || a.username || '';
|
||||
const bName = b.name || b.username || '';
|
||||
return aName.localeCompare(bName);
|
||||
}),
|
||||
...serviceAccounts
|
||||
.map((serviceAccount: IServiceAccount) => ({
|
||||
...serviceAccount,
|
||||
type: 'SERVICE ACCOUNTS',
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const aName = a.name || a.username || '';
|
||||
const bName = b.name || b.username || '';
|
||||
return aName.localeCompare(bName);
|
||||
}),
|
||||
];
|
||||
|
||||
const isLargeList = options.length > 200;
|
||||
|
||||
return (
|
||||
<StyledGroupFormUsersSelect>
|
||||
<Autocomplete
|
||||
data-testid={UG_USERS_ID}
|
||||
size='small'
|
||||
multiple
|
||||
limitTags={1}
|
||||
openOnFocus
|
||||
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={(props, option, { selected }) =>
|
||||
renderOption(props, option as UserOption, selected)
|
||||
}
|
||||
filterOptions={(options, { inputValue }) =>
|
||||
options
|
||||
.filter(
|
||||
{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),
|
||||
)
|
||||
.slice(0, 100)
|
||||
}
|
||||
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)}
|
||||
/>
|
||||
}
|
||||
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'}
|
||||
/>
|
||||
)}
|
||||
</StyledGroupFormUsersSelect>
|
||||
);
|
||||
};
|
||||
|
@ -2768,6 +2768,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-virtual@npm:^3.11.3":
|
||||
version: 3.11.3
|
||||
resolution: "@tanstack/react-virtual@npm:3.11.3"
|
||||
dependencies:
|
||||
"@tanstack/virtual-core": "npm:3.11.3"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/9718379045ecda92d2c59f3c25699c703d98509ea569d7bfb0dbf78f1e0f46f0023d7a5d3a373fb7cdd4507286c1cf47648b5c0f4b1bdb85b2d2a6c26814b884
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/table-core@npm:8.20.5":
|
||||
version: 8.20.5
|
||||
resolution: "@tanstack/table-core@npm:8.20.5"
|
||||
@ -2775,6 +2787,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/virtual-core@npm:3.11.3":
|
||||
version: 3.11.3
|
||||
resolution: "@tanstack/virtual-core@npm:3.11.3"
|
||||
checksum: 10c0/94701b8d2da9167c8b4ba36bdaff22019b8ebb19224c357c1af16cbc375b39076ecc021a8c9581001607afc921d0a843019cb27168999065dda511c445a1a335
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/dom@npm:10.4.0":
|
||||
version: 10.4.0
|
||||
resolution: "@testing-library/dom@npm:10.4.0"
|
||||
@ -10165,6 +10184,7 @@ __metadata:
|
||||
"@mui/material": "npm:5.15.3"
|
||||
"@mui/x-date-pickers": "npm:^7.0.0"
|
||||
"@tanstack/react-table": "npm:^8.10.7"
|
||||
"@tanstack/react-virtual": "npm:^3.11.3"
|
||||
"@testing-library/dom": "npm:10.4.0"
|
||||
"@testing-library/jest-dom": "npm:6.6.3"
|
||||
"@testing-library/react": "npm:16.1.0"
|
||||
|
Loading…
Reference in New Issue
Block a user