@@ -2,21 +2,15 @@ import CircularProgress from "@mui/material/CircularProgress";
2
2
import { makeStyles } from "@mui/styles" ;
3
3
import TextField from "@mui/material/TextField" ;
4
4
import Autocomplete from "@mui/material/Autocomplete" ;
5
- import { useMachine } from "@xstate/react" ;
6
5
import { User } from "api/typesGenerated" ;
7
6
import { Avatar } from "components/Avatar/Avatar" ;
8
7
import { AvatarData } from "components/AvatarData/AvatarData" ;
9
- import {
10
- ChangeEvent ,
11
- ComponentProps ,
12
- FC ,
13
- useEffect ,
14
- useRef ,
15
- useState ,
16
- } from "react" ;
17
- import { searchUserMachine } from "xServices/users/searchUserXService" ;
8
+ import { ChangeEvent , ComponentProps , FC , useState } from "react" ;
18
9
import Box from "@mui/material/Box" ;
19
10
import { useDebouncedFunction } from "hooks/debounce" ;
11
+ import { useQuery } from "@tanstack/react-query" ;
12
+ import { users } from "api/queries/users" ;
13
+ import { prepareQuery } from "utils/filters" ;
20
14
21
15
export type UserAutocompleteProps = {
22
16
value : User | null ;
@@ -34,53 +28,57 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
34
28
size = "small" ,
35
29
} ) => {
36
30
const styles = useStyles ( ) ;
37
- const [ isAutocompleteOpen , setIsAutocompleteOpen ] = useState ( false ) ;
38
- const [ searchState , sendSearch ] = useMachine ( searchUserMachine ) ;
39
- const { searchResults } = searchState . context ;
31
+ const [ autoComplete , setAutoComplete ] = useState < {
32
+ value : string ;
33
+ open : boolean ;
34
+ } > ( {
35
+ value : value ?. email ?? "" ,
36
+ open : false ,
37
+ } ) ;
38
+ const usersQuery = useQuery ( {
39
+ ...users ( {
40
+ q : prepareQuery ( encodeURI ( autoComplete . value ) ) ,
41
+ limit : 25 ,
42
+ } ) ,
43
+ enabled : autoComplete . open ,
44
+ keepPreviousData : true ,
45
+ } ) ;
40
46
41
- // Seed list of options on the first page load if a user passes in a value.
42
- // Since some organizations have long lists of users, we do not want to load
43
- // all options on page load.
44
- const onMountRef = useRef ( value ) ;
45
- useEffect ( ( ) => {
46
- const mountValue = onMountRef . current ;
47
- if ( mountValue ) {
48
- sendSearch ( "SEARCH" , { query : mountValue . email } ) ;
49
- }
50
-
51
- // This isn't in XState's docs, but its source code guarantees that the
52
- // memory reference of sendSearch will stay stable across renders. This
53
- // useEffect call will behave like an on-mount effect and will not ever need
54
- // to resynchronize
55
- } , [ sendSearch ] ) ;
56
-
57
- const { debounced : debouncedOnChange } = useDebouncedFunction (
47
+ const { debounced : debouncedInputOnChange } = useDebouncedFunction (
58
48
( event : ChangeEvent < HTMLInputElement > ) => {
59
- sendSearch ( "SEARCH" , { query : event . target . value } ) ;
49
+ setAutoComplete ( ( state ) => ( {
50
+ ...state ,
51
+ value : event . target . value ,
52
+ } ) ) ;
60
53
} ,
61
- 1000 ,
54
+ 750 ,
62
55
) ;
63
56
64
57
return (
65
58
< Autocomplete
66
- noOptionsText = "Start typing to search..."
59
+ // Since the values are filtered by the API we don't need to filter them
60
+ // in the FE because it can causes some mismatches.
61
+ filterOptions = { ( user ) => user }
62
+ noOptionsText = "No users found"
67
63
className = { className }
68
- options = { searchResults ?? [ ] }
69
- loading = { searchState . matches ( "searching" ) }
64
+ options = { usersQuery . data ?. users ?? [ ] }
65
+ loading = { usersQuery . isLoading }
70
66
value = { value }
71
67
id = "user-autocomplete"
72
- open = { isAutocompleteOpen }
68
+ open = { autoComplete . open }
73
69
onOpen = { ( ) => {
74
- setIsAutocompleteOpen ( true ) ;
70
+ setAutoComplete ( ( state ) => ( {
71
+ ...state ,
72
+ open : true ,
73
+ } ) ) ;
75
74
} }
76
75
onClose = { ( ) => {
77
- setIsAutocompleteOpen ( false ) ;
76
+ setAutoComplete ( {
77
+ value : value ?. email ?? "" ,
78
+ open : false ,
79
+ } ) ;
78
80
} }
79
81
onChange = { ( _ , newValue ) => {
80
- if ( newValue === null ) {
81
- sendSearch ( "CLEAR_RESULTS" ) ;
82
- }
83
-
84
82
onChange ( newValue ) ;
85
83
} }
86
84
isOptionEqualToValue = { ( option : User , value : User ) =>
@@ -97,39 +95,37 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
97
95
</ Box >
98
96
) }
99
97
renderInput = { ( params ) => (
100
- < >
101
- < TextField
102
- { ...params }
103
- fullWidth
104
- size = { size }
105
- label = { label }
106
- placeholder = "User email or username"
107
- className = { styles . textField }
108
- InputProps = { {
109
- ...params . InputProps ,
110
- onChange : debouncedOnChange ,
111
- startAdornment : value && (
112
- < Avatar size = "sm" src = { value . avatar_url } >
113
- { value . username }
114
- </ Avatar >
115
- ) ,
116
- endAdornment : (
117
- < >
118
- { searchState . matches ( "searching" ) ? (
119
- < CircularProgress size = { 16 } />
120
- ) : null }
121
- { params . InputProps . endAdornment }
122
- </ >
123
- ) ,
124
- classes : {
125
- root : styles . inputRoot ,
126
- } ,
127
- } }
128
- InputLabelProps = { {
129
- shrink : true ,
130
- } }
131
- />
132
- </ >
98
+ < TextField
99
+ { ...params }
100
+ fullWidth
101
+ size = { size }
102
+ label = { label }
103
+ placeholder = "User email or username"
104
+ className = { styles . textField }
105
+ InputProps = { {
106
+ ...params . InputProps ,
107
+ onChange : debouncedInputOnChange ,
108
+ startAdornment : value && (
109
+ < Avatar size = "sm" src = { value . avatar_url } >
110
+ { value . username }
111
+ </ Avatar >
112
+ ) ,
113
+ endAdornment : (
114
+ < >
115
+ { usersQuery . isFetching && autoComplete . open ? (
116
+ < CircularProgress size = { 16 } />
117
+ ) : null }
118
+ { params . InputProps . endAdornment }
119
+ </ >
120
+ ) ,
121
+ classes : {
122
+ root : styles . inputRoot ,
123
+ } ,
124
+ } }
125
+ InputLabelProps = { {
126
+ shrink : true ,
127
+ } }
128
+ />
133
129
) }
134
130
/>
135
131
) ;
0 commit comments