Skip to content

Commit e0ea8ec

Browse files
committed
Add user auto complete component
1 parent 85e05c3 commit e0ea8ec

File tree

2 files changed

+109
-78
lines changed

2 files changed

+109
-78
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import CircularProgress from "@material-ui/core/CircularProgress"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import TextField from "@material-ui/core/TextField"
4+
import Autocomplete from "@material-ui/lab/Autocomplete"
5+
import { useMachine } from "@xstate/react"
6+
import { User } from "api/typesGenerated"
7+
import { AvatarData } from "components/AvatarData/AvatarData"
8+
import debounce from "just-debounce-it"
9+
import { ChangeEvent, useState } from "react"
10+
import { searchUserMachine } from "xServices/users/searchUserXService"
11+
12+
export type UserAutocompleteProps = {
13+
value: User | null
14+
onChange: (user: User | null) => void
15+
}
16+
17+
export const UserAutocomplete: React.FC<UserAutocompleteProps> = ({ value, onChange }) => {
18+
const styles = useStyles()
19+
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
20+
const [searchState, sendSearch] = useMachine(searchUserMachine)
21+
const { searchResults } = searchState.context
22+
23+
const handleFilterChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
24+
sendSearch("SEARCH", { query: event.target.value })
25+
}, 1000)
26+
27+
return (
28+
<Autocomplete
29+
value={value}
30+
id="user-autocomplete"
31+
style={{ width: 300 }}
32+
open={isAutocompleteOpen}
33+
onOpen={() => {
34+
setIsAutocompleteOpen(true)
35+
}}
36+
onClose={() => {
37+
setIsAutocompleteOpen(false)
38+
}}
39+
onChange={(event, newValue) => {
40+
onChange(newValue)
41+
}}
42+
getOptionSelected={(option: User, value: User) => option.username === value.username}
43+
getOptionLabel={(option) => option.email}
44+
renderOption={(option: User) => (
45+
<AvatarData
46+
title={option.username}
47+
subtitle={option.email}
48+
highlightTitle
49+
avatar={
50+
option.avatar_url ? (
51+
<img
52+
className={styles.avatar}
53+
alt={`${option.username}'s Avatar`}
54+
src={option.avatar_url}
55+
/>
56+
) : null
57+
}
58+
/>
59+
)}
60+
options={searchResults}
61+
loading={searchState.matches("searching")}
62+
className={styles.autocomplete}
63+
renderInput={(params) => (
64+
<TextField
65+
{...params}
66+
margin="none"
67+
variant="outlined"
68+
placeholder="User email or username"
69+
InputProps={{
70+
...params.InputProps,
71+
onChange: handleFilterChange,
72+
endAdornment: (
73+
<>
74+
{searchState.matches("searching") ? <CircularProgress size={16} /> : null}
75+
{params.InputProps.endAdornment}
76+
</>
77+
),
78+
}}
79+
/>
80+
)}
81+
/>
82+
)
83+
}
84+
export const useStyles = makeStyles((theme) => {
85+
return {
86+
autocomplete: {
87+
"& .MuiInputBase-root": {
88+
width: 300,
89+
// Match button small height
90+
height: 36,
91+
},
92+
93+
"& input": {
94+
fontSize: 14,
95+
padding: `${theme.spacing(0, 0.5, 0, 0.5)} !important`,
96+
},
97+
},
98+
99+
avatar: {
100+
width: theme.spacing(4.5),
101+
height: theme.spacing(4.5),
102+
borderRadius: "100%",
103+
},
104+
}
105+
})

site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx

Lines changed: 4 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import CircularProgress from "@material-ui/core/CircularProgress"
21
import MenuItem from "@material-ui/core/MenuItem"
32
import Select from "@material-ui/core/Select"
43
import { makeStyles } from "@material-ui/core/styles"
@@ -8,10 +7,7 @@ import TableCell from "@material-ui/core/TableCell"
87
import TableContainer from "@material-ui/core/TableContainer"
98
import TableHead from "@material-ui/core/TableHead"
109
import TableRow from "@material-ui/core/TableRow"
11-
import TextField from "@material-ui/core/TextField"
1210
import PersonAdd from "@material-ui/icons/PersonAdd"
13-
import Autocomplete from "@material-ui/lab/Autocomplete"
14-
import { useMachine } from "@xstate/react"
1511
import { TemplateRole, TemplateUser, User } from "api/typesGenerated"
1612
import { AvatarData } from "components/AvatarData/AvatarData"
1713
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
@@ -21,25 +17,17 @@ import { LoadingButton } from "components/LoadingButton/LoadingButton"
2117
import { Stack } from "components/Stack/Stack"
2218
import { TableLoader } from "components/TableLoader/TableLoader"
2319
import { TableRowMenu } from "components/TableRowMenu/TableRowMenu"
24-
import debounce from "just-debounce-it"
25-
import { ChangeEvent, FC, useState } from "react"
26-
import { searchUserMachine } from "xServices/users/searchUserXService"
20+
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
21+
import { FC, useState } from "react"
2722

2823
const AddTemplateUser: React.FC<{
2924
isLoading: boolean
3025
onSubmit: (user: User, role: TemplateRole, reset: () => void) => void
3126
}> = ({ isLoading, onSubmit }) => {
3227
const styles = useStyles()
33-
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
34-
const [searchState, sendSearch] = useMachine(searchUserMachine)
35-
const { searchResults } = searchState.context
3628
const [selectedUser, setSelectedUser] = useState<User | null>(null)
3729
const [selectedRole, setSelectedRole] = useState<TemplateRole>("read")
3830

39-
const handleFilterChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
40-
sendSearch("SEARCH", { query: event.target.value })
41-
}, 1000)
42-
4331
const resetValues = () => {
4432
setSelectedUser(null)
4533
setSelectedRole("read")
@@ -56,60 +44,11 @@ const AddTemplateUser: React.FC<{
5644
}}
5745
>
5846
<Stack direction="row" alignItems="center" spacing={1}>
59-
<Autocomplete
47+
<UserAutocomplete
6048
value={selectedUser}
61-
disabled={isLoading}
62-
id="asynchronous-demo"
63-
style={{ width: 300 }}
64-
open={isAutocompleteOpen}
65-
onOpen={() => {
66-
setIsAutocompleteOpen(true)
67-
}}
68-
onClose={() => {
69-
setIsAutocompleteOpen(false)
70-
}}
71-
onChange={(event, newValue) => {
49+
onChange={(newValue) => {
7250
setSelectedUser(newValue)
7351
}}
74-
getOptionSelected={(option: User, value: User) => option.username === value.username}
75-
getOptionLabel={(option) => option.email}
76-
renderOption={(option: User) => (
77-
<AvatarData
78-
title={option.username}
79-
subtitle={option.email}
80-
highlightTitle
81-
avatar={
82-
option.avatar_url ? (
83-
<img
84-
className={styles.avatar}
85-
alt={`${option.username}'s Avatar`}
86-
src={option.avatar_url}
87-
/>
88-
) : null
89-
}
90-
/>
91-
)}
92-
options={searchResults}
93-
loading={searchState.matches("searching")}
94-
className={styles.autocomplete}
95-
renderInput={(params) => (
96-
<TextField
97-
{...params}
98-
margin="none"
99-
variant="outlined"
100-
placeholder="User email or username"
101-
InputProps={{
102-
...params.InputProps,
103-
onChange: handleFilterChange,
104-
endAdornment: (
105-
<>
106-
{searchState.matches("searching") ? <CircularProgress size={16} /> : null}
107-
{params.InputProps.endAdornment}
108-
</>
109-
),
110-
}}
111-
/>
112-
)}
11352
/>
11453

11554
<Select
@@ -273,19 +212,6 @@ export const TemplatePermissionsPageView: FC<
273212

274213
export const useStyles = makeStyles((theme) => {
275214
return {
276-
autocomplete: {
277-
"& .MuiInputBase-root": {
278-
width: 300,
279-
// Match button small height
280-
height: 36,
281-
},
282-
283-
"& input": {
284-
fontSize: 14,
285-
padding: `${theme.spacing(0, 0.5, 0, 0.5)} !important`,
286-
},
287-
},
288-
289215
select: {
290216
// Match button small height
291217
height: 36,

0 commit comments

Comments
 (0)