@@ -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,128 @@ 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 : 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
+ error = { usersQuery . error }
53
+ isFetching = { usersQuery . isFetching }
54
+ setFilter = { setFilter }
55
+ users = { usersQuery . data ?. users }
56
+ { ...props }
57
+ />
58
+ ) ;
59
+ } ;
60
+
61
+ export type MemberAutocompleteProps =
62
+ CommonAutocompleteProps < OrganizationMemberWithUserData > & {
63
+ organizationId : string ;
64
+ } ;
65
+
66
+ export const MemberAutocomplete : FC < MemberAutocompleteProps > = ( {
67
+ organizationId,
68
+ ...props
69
+ } ) => {
70
+ const [ filter , setFilter ] = useState < string > ( ) ;
71
+
72
+ // Currently this queries all members, as there is no pagination.
73
+ const membersQuery = useQuery ( {
74
+ ...organizationMembers ( organizationId ) ,
75
+ enabled : filter !== undefined ,
49
76
keepPreviousData : true ,
50
77
} ) ;
78
+ return (
79
+ < InnerAutocomplete < OrganizationMemberWithUserData >
80
+ error = { membersQuery . error }
81
+ isFetching = { membersQuery . isFetching }
82
+ setFilter = { setFilter }
83
+ users = { membersQuery . data }
84
+ { ...props }
85
+ />
86
+ ) ;
87
+ } ;
88
+
89
+ type InnerAutocompleteProps < T extends SelectedUser > =
90
+ CommonAutocompleteProps < T > & {
91
+ /** The error is null if not loaded or no error. */
92
+ error : unknown ;
93
+ isFetching : boolean ;
94
+ /** Filter is undefined if the autocomplete is closed. */
95
+ setFilter : ( filter : string | undefined ) => void ;
96
+ /** Users are undefined if not loaded or errored. */
97
+ users : readonly T [ ] | undefined ;
98
+ } ;
99
+
100
+ const InnerAutocomplete = < T extends SelectedUser > ( {
101
+ className,
102
+ error,
103
+ isFetching,
104
+ label,
105
+ onChange,
106
+ required,
107
+ setFilter,
108
+ size = "small" ,
109
+ users,
110
+ value,
111
+ } : InnerAutocompleteProps < T > ) => {
112
+ const [ open , setOpen ] = useState ( false ) ;
51
113
52
114
const { debounced : debouncedInputOnChange } = useDebouncedFunction (
53
115
( event : ChangeEvent < HTMLInputElement > ) => {
54
- setAutoComplete ( ( state ) => ( {
55
- ...state ,
56
- value : event . target . value ,
57
- } ) ) ;
116
+ setFilter ( event . target . value ?? "" ) ;
58
117
} ,
59
118
750 ,
60
119
) ;
61
120
62
121
return (
63
122
< Autocomplete
64
- noOptionsText = "No users found"
123
+ noOptionsText = {
124
+ error
125
+ ? getErrorMessage ( error , "Unable to fetch users" )
126
+ : "No users found"
127
+ }
65
128
className = { className }
66
- options = { usersQuery . data ?. users ?? [ ] }
67
- loading = { usersQuery . isLoading }
129
+ options = { users ?? [ ] }
130
+ loading = { ! users && ! error }
68
131
value = { value }
69
132
data-testid = "user-autocomplete"
70
- open = { autoComplete . open }
133
+ open = { open }
71
134
isOptionEqualToValue = { ( a , b ) => a . username === b . username }
72
135
getOptionLabel = { ( option ) => option . email }
73
136
onOpen = { ( ) => {
74
- setAutoComplete ( ( state ) => ( {
75
- ...state ,
76
- open : true ,
77
- } ) ) ;
137
+ setOpen ( true ) ;
138
+ setFilter ( value ?. email ?? "" ) ;
78
139
} }
79
140
onClose = { ( ) => {
80
- setAutoComplete ( {
81
- value : value ?. email ?? "" ,
82
- open : false ,
83
- } ) ;
141
+ setOpen ( false ) ;
142
+ setFilter ( undefined ) ;
84
143
} }
85
144
onChange = { ( _ , newValue ) => {
86
145
onChange ( newValue ) ;
@@ -117,9 +176,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
117
176
) ,
118
177
endAdornment : (
119
178
< >
120
- { usersQuery . isFetching && autoComplete . open && (
121
- < CircularProgress size = { 16 } />
122
- ) }
179
+ { isFetching && open && < CircularProgress size = { 16 } /> }
123
180
{ params . InputProps . endAdornment }
124
181
</ >
125
182
) ,
0 commit comments