diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx new file mode 100644 index 0000000000000..2cd30da85c32b --- /dev/null +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -0,0 +1,125 @@ +import CircularProgress from "@material-ui/core/CircularProgress" +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import Autocomplete from "@material-ui/lab/Autocomplete" +import { useMachine } from "@xstate/react" +import { User } from "api/typesGenerated" +import { AvatarData } from "components/AvatarData/AvatarData" +import debounce from "just-debounce-it" +import { ChangeEvent, useEffect, useState } from "react" +import { searchUserMachine } from "xServices/users/searchUserXService" + +export type UserAutocompleteProps = { + value?: User + onChange: (user: User | null) => void +} + +export const UserAutocomplete: React.FC = ({ value, onChange }) => { + const styles = useStyles() + const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false) + const [searchState, sendSearch] = useMachine(searchUserMachine) + const { searchResults } = searchState.context + const [selectedValue, setSelectedValue] = useState(value || null) + + // seed list of options on the first page load if a user pases in a value + // since some organizations have long lists of users, we do not load all options on page load. + useEffect(() => { + if (value) { + sendSearch("SEARCH", { query: value.email }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const handleFilterChange = debounce((event: ChangeEvent) => { + sendSearch("SEARCH", { query: event.target.value }) + }, 1000) + + return ( + { + setIsAutocompleteOpen(true) + }} + onClose={() => { + setIsAutocompleteOpen(false) + }} + onChange={(_, newValue) => { + if (newValue === null) { + sendSearch("CLEAR_RESULTS") + } + + setSelectedValue(newValue) + onChange(newValue) + }} + getOptionSelected={(option: User, value: User) => option.username === value.username} + getOptionLabel={(option) => option.email} + renderOption={(option: User) => ( + + ) : null + } + /> + )} + options={searchResults} + loading={searchState.matches("searching")} + className={styles.autocomplete} + renderInput={(params) => ( + + {searchState.matches("searching") ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + ) +} +export const useStyles = makeStyles((theme) => { + return { + autocomplete: { + width: "100%", + + "& .MuiFormControl-root": { + width: "100%", + }, + + "& .MuiInputBase-root": { + width: "100%", + // Match button small height + height: 40, + }, + + "& input": { + fontSize: 14, + padding: `${theme.spacing(0, 0.5, 0, 0.5)} !important`, + }, + }, + + avatar: { + width: theme.spacing(4.5), + height: theme.spacing(4.5), + borderRadius: "100%", + }, + } +}) diff --git a/site/src/xServices/users/searchUserXService.ts b/site/src/xServices/users/searchUserXService.ts new file mode 100644 index 0000000000000..28fcb043d8e00 --- /dev/null +++ b/site/src/xServices/users/searchUserXService.ts @@ -0,0 +1,61 @@ +import { getUsers } from "api/api" +import { User } from "api/typesGenerated" +import { queryToFilter } from "util/filters" +import { assign, createMachine } from "xstate" + +export type AutocompleteEvent = { type: "SEARCH"; query: string } | { type: "CLEAR_RESULTS" } + +export const searchUserMachine = createMachine( + { + id: "searchUserMachine", + schema: { + context: {} as { + searchResults?: User[] + }, + events: {} as AutocompleteEvent, + services: {} as { + searchUsers: { + data: User[] + } + }, + }, + context: { + searchResults: [], + }, + tsTypes: {} as import("./searchUserXService.typegen").Typegen0, + initial: "idle", + states: { + idle: { + on: { + SEARCH: "searching", + CLEAR_RESULTS: { + actions: ["clearResults"], + target: "idle", + }, + }, + }, + searching: { + invoke: { + src: "searchUsers", + onDone: { + target: "idle", + actions: ["assignSearchResults"], + }, + }, + }, + }, + }, + { + services: { + searchUsers: (_, { query }) => getUsers(queryToFilter(query)), + }, + actions: { + assignSearchResults: assign({ + searchResults: (_, { data }) => data, + }), + clearResults: assign({ + searchResults: (_) => undefined, + }), + }, + }, +) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy