@@ -2,8 +2,10 @@ import { css } from "@emotion/css";
2
2
import Autocomplete from "@mui/material/Autocomplete" ;
3
3
import CircularProgress from "@mui/material/CircularProgress" ;
4
4
import TextField from "@mui/material/TextField" ;
5
+ import { getErrorMessage } from "api/errors" ;
6
+ import { organizationMembers } from "api/queries/organizations" ;
5
7
import { users } from "api/queries/users" ;
6
- import type { User } from "api/typesGenerated" ;
8
+ import type { OrganizationMemberWithUserData , User } from "api/typesGenerated" ;
7
9
import { Avatar } from "components/Avatar/Avatar" ;
8
10
import { AvatarData } from "components/AvatarData/AvatarData" ;
9
11
import { useDebouncedFunction } from "hooks/debounce" ;
@@ -16,71 +18,124 @@ import {
16
18
import { useQuery } from "react-query" ;
17
19
import { prepareQuery } from "utils/filters" ;
18
20
19
- export type UserAutocompleteProps = {
20
- value : User | null ;
21
- onChange : ( user : User | null ) => void ;
22
- label ?: string ;
21
+ // The common properties between users and org members that we need.
22
+ export type SelectedUser = {
23
+ avatar_url : string ;
24
+ email : string ;
25
+ username : string ;
26
+ } ;
27
+
28
+ export type CommonAutocompleteProps < T extends SelectedUser > = {
23
29
className ?: string ;
24
- size ?: ComponentProps < typeof TextField > [ "size" ] ;
30
+ label ?: string ;
31
+ onChange : ( user : T | null ) => void ;
25
32
required ?: boolean ;
33
+ size ?: ComponentProps < typeof TextField > [ "size" ] ;
34
+ value : T | null ;
26
35
} ;
27
36
28
- export const UserAutocomplete : FC < UserAutocompleteProps > = ( {
29
- value,
30
- onChange,
31
- label,
32
- className,
33
- size = "small" ,
34
- required,
35
- } ) => {
36
- const [ autoComplete , setAutoComplete ] = useState < {
37
- value : string ;
38
- open : boolean ;
39
- } > ( {
40
- value : value ?. email ?? "" ,
41
- open : false ,
42
- } ) ;
37
+ export type UserAutocompleteProps = CommonAutocompleteProps < User > ;
38
+
39
+ export const UserAutocomplete : FC < UserAutocompleteProps > = ( props ) => {
40
+ const [ filter , setFilter ] = useState < string > ( ) ;
41
+
43
42
const usersQuery = useQuery ( {
44
43
...users ( {
45
- q : prepareQuery ( encodeURI ( autoComplete . value ) ) ,
44
+ q : filter !== undefined ? prepareQuery ( encodeURI ( filter ) ) : "" ,
46
45
limit : 25 ,
47
46
} ) ,
48
- enabled : autoComplete . open ,
47
+ enabled : filter !== undefined ,
48
+ keepPreviousData : true ,
49
+ } ) ;
50
+ return (
51
+ < InnerAutocomplete < User >
52
+ users = { usersQuery . data ?. users }
53
+ error = { usersQuery . error }
54
+ setFilter = { setFilter }
55
+ { ...props }
56
+ />
57
+ ) ;
58
+ } ;
59
+
60
+ export type MemberAutocompleteProps =
61
+ CommonAutocompleteProps < OrganizationMemberWithUserData > & {
62
+ organizationId : string ;
63
+ } ;
64
+
65
+ export const MemberAutocomplete : FC < MemberAutocompleteProps > = ( {
66
+ organizationId,
67
+ ...props
68
+ } ) => {
69
+ const [ filter , setFilter ] = useState < string > ( ) ;
70
+
71
+ // Currently this queries all members, as there is no pagination.
72
+ const membersQuery = useQuery ( {
73
+ ...organizationMembers ( organizationId ) ,
74
+ enabled : filter !== undefined ,
49
75
keepPreviousData : true ,
50
76
} ) ;
77
+ return (
78
+ < InnerAutocomplete < OrganizationMemberWithUserData >
79
+ users = { membersQuery . data }
80
+ error = { membersQuery . error }
81
+ setFilter = { setFilter }
82
+ { ...props }
83
+ />
84
+ ) ;
85
+ } ;
86
+
87
+ type InnerAutocompleteProps < T extends SelectedUser > =
88
+ CommonAutocompleteProps < T > & {
89
+ /** Filter is undefined if the autocomplete is closed. */
90
+ setFilter : ( filter : string | undefined ) => void ;
91
+ /** Users are undefined if not loaded or errored. */
92
+ users : readonly T [ ] | undefined ;
93
+ /** The error is null if not loaded or no error. */
94
+ error : unknown ;
95
+ } ;
96
+
97
+ const InnerAutocomplete = < T extends SelectedUser > ( {
98
+ className,
99
+ error,
100
+ label,
101
+ onChange,
102
+ required,
103
+ setFilter,
104
+ size = "small" ,
105
+ users,
106
+ value,
107
+ } : InnerAutocompleteProps < T > ) => {
108
+ const [ open , setOpen ] = useState ( false ) ;
51
109
52
110
const { debounced : debouncedInputOnChange } = useDebouncedFunction (
53
111
( event : ChangeEvent < HTMLInputElement > ) => {
54
- setAutoComplete ( ( state ) => ( {
55
- ...state ,
56
- value : event . target . value ,
57
- } ) ) ;
112
+ setFilter ( event . target . value ?? "" ) ;
58
113
} ,
59
114
750 ,
60
115
) ;
61
116
62
117
return (
63
118
< Autocomplete
64
- noOptionsText = "No users found"
119
+ noOptionsText = {
120
+ error
121
+ ? getErrorMessage ( error , "Unable to fetch users" )
122
+ : "No users found"
123
+ }
65
124
className = { className }
66
- options = { usersQuery . data ?. users ?? [ ] }
67
- loading = { usersQuery . isLoading }
125
+ options = { users ?? [ ] }
126
+ loading = { ! users && ! error }
68
127
value = { value }
69
128
data-testid = "user-autocomplete"
70
- open = { autoComplete . open }
129
+ open = { open }
71
130
isOptionEqualToValue = { ( a , b ) => a . username === b . username }
72
131
getOptionLabel = { ( option ) => option . email }
73
132
onOpen = { ( ) => {
74
- setAutoComplete ( ( state ) => ( {
75
- ...state ,
76
- open : true ,
77
- } ) ) ;
133
+ setOpen ( true ) ;
134
+ setFilter ( value ?. email ?? "" ) ;
78
135
} }
79
136
onClose = { ( ) => {
80
- setAutoComplete ( {
81
- value : value ?. email ?? "" ,
82
- open : false ,
83
- } ) ;
137
+ setOpen ( false ) ;
138
+ setFilter ( undefined ) ;
84
139
} }
85
140
onChange = { ( _ , newValue ) => {
86
141
onChange ( newValue ) ;
@@ -117,9 +172,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
117
172
) ,
118
173
endAdornment : (
119
174
< >
120
- { usersQuery . isFetching && autoComplete . open && (
121
- < CircularProgress size = { 16 } />
122
- ) }
175
+ { ! users && ! error && open && < CircularProgress size = { 16 } /> }
123
176
{ params . InputProps . endAdornment }
124
177
</ >
125
178
) ,
0 commit comments