diff --git a/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx b/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx index 80d2310f186..a5d6b9c347b 100644 --- a/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx +++ b/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx @@ -1,5 +1,6 @@ import { useAccountUsersInfiniteQuery, + useAllAccountUsersQuery, useUpdateChildAccountDelegatesQuery, } from '@linode/queries'; import { ActionsPanel, Autocomplete, Notice, Typography } from '@linode/ui'; @@ -41,6 +42,7 @@ export const UpdateDelegationForm = ({ }: DelegationsFormProps) => { const theme = useTheme(); const [inputValue, setInputValue] = React.useState(''); + const [allUserSelected, setAllUserSelected] = React.useState(false); const debouncedInputValue = useDebouncedValue(inputValue); const { data: permissions } = usePermissions('account', [ @@ -55,13 +57,35 @@ export const UpdateDelegationForm = ({ const { data, error, fetchNextPage, hasNextPage, isFetching } = useAccountUsersInfiniteQuery(apiFilter); + const { + data: allUsers, + isFetching: isFetchingAllUsers, + refetch: refetchAllUsers, + } = useAllAccountUsersQuery(allUserSelected, { + user_type: 'parent', + }); + const users = - data?.pages.flatMap((page) => { - return page.data.map((user) => ({ - label: user.username, - value: user.username, - })); - }) ?? []; + allUserSelected && allUsers + ? allUsers.map((user) => ({ + label: user.username, + value: user.username, + })) + : (data?.pages.flatMap((page) => { + return page.data.map((user) => ({ + label: user.username, + value: user.username, + })); + }) ?? []); + + const isSearching = + inputValue.length > 0 && debouncedInputValue !== inputValue; + + const isLoadingOptions = isFetching || isFetchingAllUsers; + + const showNoOptionsText = !isLoadingOptions && !isSearching; + + const isSelectAllFetching = allUserSelected && isFetchingAllUsers; const { mutateAsync: updateDelegates } = useUpdateChildAccountDelegatesQuery(); @@ -78,6 +102,7 @@ export const UpdateDelegationForm = ({ handleSubmit, reset, setError, + setValue, } = form; const onSubmit = async (values: UpdateDelegationsFormValues) => { @@ -99,9 +124,21 @@ export const UpdateDelegationForm = ({ } }; + const onSelectAllClick = async () => { + setAllUserSelected(true); + const { data } = await refetchAllUsers(); + if (data) { + setValue( + 'users', + data.map((user) => ({ label: user.username, value: user.username })) + ); + } + }; + const handleClose = () => { reset(); onClose(); + setAllUserSelected(false); }; return ( @@ -135,20 +172,27 @@ export const UpdateDelegationForm = ({ render={({ field, fieldState }) => ( option.value === value.value } - label={'Delegate Users'} - loading={isFetching} + label="Delegate Users" + loading={isFetching || isFetchingAllUsers} multiple noMarginTop + noOptionsText={showNoOptionsText ? 'No users found' : ' '} onChange={(_, newValue) => { field.onChange(newValue || []); }} onInputChange={(_, value) => { setInputValue(value); }} + onSelectAllClick={(isSelectAllActive) => { + if (isSelectAllActive && !allUserSelected) { + onSelectAllClick(); + } + }} options={users} placeholder={getPlaceholder( 'delegates', @@ -171,6 +215,12 @@ export const UpdateDelegationForm = ({ }} textFieldProps={{ hideLabel: true, + helperText: isSelectAllFetching + ? 'Fetching all users...' + : undefined, + InputProps: isSelectAllFetching + ? { startAdornment: null } + : undefined, }} value={field.value} /> diff --git a/packages/ui/src/components/Autocomplete/Autocomplete.tsx b/packages/ui/src/components/Autocomplete/Autocomplete.tsx index 2dd35db0ca3..060b7888848 100644 --- a/packages/ui/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/ui/src/components/Autocomplete/Autocomplete.tsx @@ -48,6 +48,10 @@ export interface EnhancedAutocompleteProps< noMarginTop?: boolean; /** Element to show when the Autocomplete search yields no results. */ noOptionsText?: JSX.Element | string; + /** Handler called when the Select All option is clicked. */ + onSelectAllClick?: ( + event: React.MouseEvent, + ) => void; placeholder?: string; renderInput?: (_params: AutocompleteRenderInputParams) => React.ReactNode; /** Label for the "select all" option. */ @@ -96,6 +100,7 @@ export const Autocomplete = < keepSearchEnabledOnMobile = false, onBlur, onChange, + onSelectAllClick, options, placeholder, renderInput, @@ -191,6 +196,18 @@ export const Autocomplete = < const isSelectAllOption = option === selectAllOption; const ListItem = isSelectAllOption ? StyledListItem : 'li'; + // If this is the Select All option, add a click handler + const handleClick = ( + event: React.MouseEvent, + ) => { + if (isSelectAllOption && onSelectAllClick) { + onSelectAllClick(event); + } + if (props.onClick) { + props.onClick(event); + } + }; + return renderOption ? ( renderOption(props, option, state, ownerState) ) : ( @@ -198,9 +215,10 @@ export const Autocomplete = < {...props} data-pendo-id={ rest.getOptionLabel ? rest.getOptionLabel(option) : option.label - } // Adding data-pendo-id for better tracking in Pendo analytics, using the option label as the identifier for the option element. + } data-qa-option key={props.key} + onClick={isSelectAllOption ? handleClick : props.onClick} > <>