import { css } from "@emotion/css"; import Autocomplete from "@mui/material/Autocomplete"; import CircularProgress from "@mui/material/CircularProgress"; import TextField from "@mui/material/TextField"; import { getErrorMessage } from "api/errors"; import { organizationMembers } from "api/queries/organizations"; import { users } from "api/queries/users"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import { AvatarData } from "components/AvatarData/AvatarData"; import { Avatar } from "components/deprecated/Avatar/Avatar"; import { useDebouncedFunction } from "hooks/debounce"; import { type ChangeEvent, type ComponentProps, type FC, useState, } from "react"; import { useQuery } from "react-query"; import { prepareQuery } from "utils/filters"; // The common properties between users and org members that we need. export type SelectedUser = { avatar_url: string; email: string; username: string; }; export type CommonAutocompleteProps = { className?: string; label?: string; onChange: (user: T | null) => void; required?: boolean; size?: ComponentProps["size"]; value: T | null; }; export type UserAutocompleteProps = CommonAutocompleteProps; export const UserAutocomplete: FC = (props) => { const [filter, setFilter] = useState(); const usersQuery = useQuery({ ...users({ q: prepareQuery(encodeURI(filter ?? "")), limit: 25, }), enabled: filter !== undefined, keepPreviousData: true, }); return ( error={usersQuery.error} isFetching={usersQuery.isFetching} setFilter={setFilter} users={usersQuery.data?.users} {...props} /> ); }; export type MemberAutocompleteProps = CommonAutocompleteProps & { organizationId: string; }; export const MemberAutocomplete: FC = ({ organizationId, ...props }) => { const [filter, setFilter] = useState(); // Currently this queries all members, as there is no pagination. const membersQuery = useQuery({ ...organizationMembers(organizationId), enabled: filter !== undefined, keepPreviousData: true, }); return ( error={membersQuery.error} isFetching={membersQuery.isFetching} setFilter={setFilter} users={membersQuery.data} {...props} /> ); }; type InnerAutocompleteProps = CommonAutocompleteProps & { /** The error is null if not loaded or no error. */ error: unknown; isFetching: boolean; /** Filter is undefined if the autocomplete is closed. */ setFilter: (filter: string | undefined) => void; /** Users are undefined if not loaded or errored. */ users: readonly T[] | undefined; }; const InnerAutocomplete = ({ className, error, isFetching, label, onChange, required, setFilter, size = "small", users, value, }: InnerAutocompleteProps) => { const [open, setOpen] = useState(false); const { debounced: debouncedInputOnChange } = useDebouncedFunction( (event: ChangeEvent) => { setFilter(event.target.value ?? ""); }, 750, ); return ( a.username === b.username} getOptionLabel={(option) => option.email} onOpen={() => { setOpen(true); setFilter(value?.email ?? ""); }} onClose={() => { setOpen(false); setFilter(undefined); }} onChange={(_, newValue) => { onChange(newValue); }} renderOption={({ key, ...props }, option) => (
  • )} renderInput={(params) => ( {value.username} ), endAdornment: ( <> {isFetching && open && } {params.InputProps.endAdornment} ), classes: { root }, }} InputLabelProps={{ shrink: true, }} /> )} /> ); }; const root = css` padding-left: 14px !important; // Same padding left as input gap: 4px; `;