From ca6a4dffadc00450274d7bdb2c771dd4730617ec Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 26 Sep 2022 19:19:49 +0000 Subject: [PATCH 1/6] chore: Add user autocomplete --- .../UserAutocomplete/UserAutocomplete.tsx | 105 ++++++++++++++++++ .../src/xServices/users/searchUserXService.ts | 55 +++++++++ 2 files changed, 160 insertions(+) create mode 100644 site/src/components/UserAutocomplete/UserAutocomplete.tsx create mode 100644 site/src/xServices/users/searchUserXService.ts diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx new file mode 100644 index 0000000000000..1e2c8f1dc5f13 --- /dev/null +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -0,0 +1,105 @@ +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, 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 handleFilterChange = debounce((event: ChangeEvent) => { + sendSearch("SEARCH", { query: event.target.value }) + }, 1000) + + return ( + { + setIsAutocompleteOpen(true) + }} + onClose={() => { + setIsAutocompleteOpen(false) + }} + onChange={(event, 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: { + "& .MuiInputBase-root": { + width: 300, + // Match button small height + height: 36, + }, + + "& 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..6d7f31d23da91 --- /dev/null +++ b/site/src/xServices/users/searchUserXService.ts @@ -0,0 +1,55 @@ +import { getUsers } from "api/api" +import { User } from "api/typesGenerated" +import { queryToFilter } from "util/filters" +import { assign, createMachine } from "xstate" + +export const searchUserMachine = createMachine( + { + id: "searchUserMachine", + schema: { + context: {} as { + searchResults: User[] + }, + events: {} as { + type: "SEARCH" + query: string + }, + services: {} as { + searchUsers: { + data: User[] + } + }, + }, + context: { + searchResults: [], + }, + tsTypes: {} as import("./searchUserXService.typegen").Typegen0, + initial: "idle", + states: { + idle: { + on: { + SEARCH: "searching", + }, + }, + searching: { + invoke: { + src: "searchUsers", + onDone: { + target: "idle", + actions: ["assignSearchResults"], + }, + }, + }, + }, + }, + { + services: { + searchUsers: (_, { query }) => getUsers(queryToFilter(query)), + }, + actions: { + assignSearchResults: assign({ + searchResults: (_, { data }) => data, + }), + }, + }, +) From 4dfc22d548af6bb619aa3fef487ff219a8e54ecb Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 26 Sep 2022 19:26:39 +0000 Subject: [PATCH 2/6] Update value type --- site/src/components/UserAutocomplete/UserAutocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index 1e2c8f1dc5f13..bc714c72abff5 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -10,7 +10,7 @@ import { ChangeEvent, useState } from "react" import { searchUserMachine } from "xServices/users/searchUserXService" export type UserAutocompleteProps = { - value: User + value: User | null onChange: (user: User | null) => void } From e276c907f9c448b2945406651e0d500c88e54739 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 26 Sep 2022 22:32:18 +0000 Subject: [PATCH 3/6] fix initial load and option updates --- .../UserAutocomplete/UserAutocomplete.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index 1e2c8f1dc5f13..e8698343cfb8f 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -6,11 +6,11 @@ import { useMachine } from "@xstate/react" import { User } from "api/typesGenerated" import { AvatarData } from "components/AvatarData/AvatarData" import debounce from "just-debounce-it" -import { ChangeEvent, useState } from "react" +import { ChangeEvent, useEffect, useState } from "react" import { searchUserMachine } from "xServices/users/searchUserXService" export type UserAutocompleteProps = { - value: User + value?: User onChange: (user: User | null) => void } @@ -19,6 +19,21 @@ export const UserAutocomplete: React.FC = ({ value, onCha 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 + useEffect(() => { + const query = value ? value.email : "" + sendSearch("SEARCH", { query }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // when selected value changes, update search terms + useEffect(() => { + const query = selectedValue ? selectedValue.email : "" + sendSearch("SEARCH", { query }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedValue]) const handleFilterChange = debounce((event: ChangeEvent) => { sendSearch("SEARCH", { query: event.target.value }) @@ -26,9 +41,8 @@ export const UserAutocomplete: React.FC = ({ value, onCha return ( { setIsAutocompleteOpen(true) @@ -37,6 +51,7 @@ export const UserAutocomplete: React.FC = ({ value, onCha setIsAutocompleteOpen(false) }} onChange={(event, newValue) => { + setSelectedValue(newValue) onChange(newValue) }} getOptionSelected={(option: User, value: User) => option.username === value.username} From 59ab375af76a777f4a09301b2f868d53294af8ef Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 26 Sep 2022 22:47:13 +0000 Subject: [PATCH 4/6] cleaned up styling --- .../components/UserAutocomplete/UserAutocomplete.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index e8698343cfb8f..71a466f2852ff 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -99,10 +99,16 @@ export const UserAutocomplete: React.FC = ({ value, onCha export const useStyles = makeStyles((theme) => { return { autocomplete: { + width: "100%", + + "& .MuiFormControl-root": { + width: "100%", + }, + "& .MuiInputBase-root": { - width: 300, + width: "100%", // Match button small height - height: 36, + height: 40, }, "& input": { From 6b8e5edfb58d5fcc9865733ac662080a516b5187 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 27 Sep 2022 13:57:40 +0000 Subject: [PATCH 5/6] PR comments --- .../UserAutocomplete/UserAutocomplete.tsx | 21 +++++++++---------- .../src/xServices/users/searchUserXService.ts | 18 +++++++++++----- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index 71a466f2852ff..2cd30da85c32b 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -21,20 +21,15 @@ export const UserAutocomplete: React.FC = ({ value, onCha const { searchResults } = searchState.context const [selectedValue, setSelectedValue] = useState(value || null) - // seed list of options on the first page load + // 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(() => { - const query = value ? value.email : "" - sendSearch("SEARCH", { query }) + if (value) { + sendSearch("SEARCH", { query: value.email }) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - // when selected value changes, update search terms - useEffect(() => { - const query = selectedValue ? selectedValue.email : "" - sendSearch("SEARCH", { query }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedValue]) - const handleFilterChange = debounce((event: ChangeEvent) => { sendSearch("SEARCH", { query: event.target.value }) }, 1000) @@ -50,7 +45,11 @@ export const UserAutocomplete: React.FC = ({ value, onCha onClose={() => { setIsAutocompleteOpen(false) }} - onChange={(event, newValue) => { + onChange={(_, newValue) => { + if (newValue === null) { + sendSearch("CLEAR_RESULTS") + } + setSelectedValue(newValue) onChange(newValue) }} diff --git a/site/src/xServices/users/searchUserXService.ts b/site/src/xServices/users/searchUserXService.ts index 6d7f31d23da91..4ec60a039adb6 100644 --- a/site/src/xServices/users/searchUserXService.ts +++ b/site/src/xServices/users/searchUserXService.ts @@ -3,17 +3,18 @@ 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 { - type: "SEARCH" - query: string + searchResults?: User[] }, + events: {} as AutocompleteEvent, services: {} as { searchUsers: { data: User[] @@ -29,6 +30,10 @@ export const searchUserMachine = createMachine( idle: { on: { SEARCH: "searching", + CLEAR_RESULTS: { + actions: ["clearResults"], + target: "idle", + } }, }, searching: { @@ -50,6 +55,9 @@ export const searchUserMachine = createMachine( assignSearchResults: assign({ searchResults: (_, { data }) => data, }), + clearResults: assign({ + searchResults: (_) => undefined, + }) }, }, ) From 133da7ea8d48c4dcf96bf7f5e12faebd38905975 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 27 Sep 2022 14:02:12 +0000 Subject: [PATCH 6/6] prettier --- site/src/xServices/users/searchUserXService.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/site/src/xServices/users/searchUserXService.ts b/site/src/xServices/users/searchUserXService.ts index 4ec60a039adb6..28fcb043d8e00 100644 --- a/site/src/xServices/users/searchUserXService.ts +++ b/site/src/xServices/users/searchUserXService.ts @@ -3,9 +3,7 @@ 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 type AutocompleteEvent = { type: "SEARCH"; query: string } | { type: "CLEAR_RESULTS" } export const searchUserMachine = createMachine( { @@ -33,7 +31,7 @@ export const searchUserMachine = createMachine( CLEAR_RESULTS: { actions: ["clearResults"], target: "idle", - } + }, }, }, searching: { @@ -57,7 +55,7 @@ export const searchUserMachine = createMachine( }), clearResults: assign({ searchResults: (_) => undefined, - }) + }), }, }, )