1
1
import MenuItem from "@mui/material/MenuItem" ;
2
2
import MenuList from "@mui/material/MenuList" ;
3
3
import { useState } from "react" ;
4
- import { useQuery } from "react-query" ;
5
- import { users } from "api/queries/users" ;
4
+ import { type QueryClient , useQuery , useQueryClient } from "react-query" ;
5
+ import { API } from "api/api" ;
6
+ import { meKey , usersKey , users as usersQuery } from "api/queries/users" ;
7
+ import type { User } from "api/typesGenerated" ;
6
8
import { Loader } from "components/Loader/Loader" ;
7
9
import { MenuButton } from "components/Menu/MenuButton" ;
8
10
import { MenuCheck } from "components/Menu/MenuCheck" ;
@@ -15,35 +17,39 @@ import {
15
17
withPopover ,
16
18
} from "components/Popover/Popover" ;
17
19
import { UserAvatar } from "components/UserAvatar/UserAvatar" ;
20
+ import { useDebouncedValue } from "hooks/debounce" ;
21
+
22
+ type UserOption = {
23
+ label : string ;
24
+ value : string ;
25
+ avatar : JSX . Element ;
26
+ } ;
18
27
19
28
type UserMenuProps = {
20
- selected : string | undefined ;
21
- onSelect : ( value : string ) => void ;
29
+ // The currently selected user email or undefined if no user is selected
30
+ selected : UserOption [ "value" ] | undefined ;
31
+ onSelect : ( value : UserOption [ "value" ] ) => void ;
22
32
} ;
23
33
24
34
export const UserMenu = withPopover < UserMenuProps > ( ( props ) => {
35
+ const queryClient = useQueryClient ( ) ;
25
36
const popover = usePopover ( ) ;
26
37
const { selected, onSelect } = props ;
27
38
const [ filter , setFilter ] = useState ( "" ) ;
28
- const userOptionsQuery = useQuery ( {
29
- ...users ( { } ) ,
30
- enabled : selected !== undefined || popover . isOpen ,
39
+ const debouncedFilter = useDebouncedValue ( filter , 300 ) ;
40
+ const usersQueryResult = useQuery ( {
41
+ ...usersQuery ( { limit : 100 , q : debouncedFilter } ) ,
42
+ enabled : popover . isOpen ,
31
43
} ) ;
32
- const options = userOptionsQuery . data ?. users
33
- . filter ( ( u ) => {
34
- const f = filter . toLowerCase ( ) ;
35
- return (
36
- u . name ?. toLowerCase ( ) . includes ( f ) ||
37
- u . username . toLowerCase ( ) . includes ( f ) ||
38
- u . email . toLowerCase ( ) . includes ( f )
39
- ) ;
40
- } )
41
- . map ( ( u ) => ( {
42
- label : u . name ?? u . username ,
43
- value : u . id ,
44
- avatar : < UserAvatar size = "xs" username = { u . username } src = { u . avatar_url } /> ,
45
- } ) ) ;
46
- const selectedOption = options ?. find ( ( option ) => option . value === selected ) ;
44
+ const { data : selectedUser } = useQuery ( {
45
+ queryKey : selectedUserKey ( selected ?? "" ) ,
46
+ queryFn : ( ) => getSelectedUser ( selected ?? "" , queryClient ) ,
47
+ enabled : selected !== undefined ,
48
+ } ) ;
49
+ const options = mountOptions ( usersQueryResult . data ?. users , selectedUser ) ;
50
+ const selectedOption = selectedUser
51
+ ? optionFromUser ( selectedUser )
52
+ : undefined ;
47
53
48
54
return (
49
55
< >
@@ -76,6 +82,17 @@ export const UserMenu = withPopover<UserMenuProps>((props) => {
76
82
selected = { isSelected }
77
83
key = { option . value }
78
84
onClick = { ( ) => {
85
+ const user = usersQueryResult . data ?. users . find (
86
+ ( u ) => u . email === option . value ,
87
+ ) ;
88
+
89
+ if ( ! user ) {
90
+ return ;
91
+ }
92
+
93
+ // This avoid the need to refetch the selected user query
94
+ // when the user is selected
95
+ setSelectedUserQueryData ( user , queryClient ) ;
79
96
popover . setIsOpen ( false ) ;
80
97
onSelect ( option . value ) ;
81
98
} }
@@ -91,9 +108,63 @@ export const UserMenu = withPopover<UserMenuProps>((props) => {
91
108
< MenuNoResults />
92
109
)
93
110
) : (
94
- < Loader />
111
+ < Loader size = { 20 } />
95
112
) }
96
113
</ PopoverContent >
97
114
</ >
98
115
) ;
99
116
} ) ;
117
+
118
+ function selectedUserKey ( email : string ) {
119
+ return usersKey ( { limit : 1 , q : email } ) ;
120
+ }
121
+
122
+ async function getSelectedUser (
123
+ email : string ,
124
+ queryClient : QueryClient ,
125
+ ) : Promise < User | undefined > {
126
+ const loggedInUser = queryClient . getQueryData < User > ( meKey ) ;
127
+
128
+ if ( loggedInUser && loggedInUser . email === email ) {
129
+ return loggedInUser ;
130
+ }
131
+
132
+ const usersRes = await API . getUsers ( { q : email , limit : 1 } ) ;
133
+ return usersRes . users . at ( 0 ) ;
134
+ }
135
+
136
+ function setSelectedUserQueryData ( user : User , queryClient : QueryClient ) {
137
+ queryClient . setQueryData ( selectedUserKey ( user . email ) , user ) ;
138
+ }
139
+
140
+ function optionFromUser ( user : User ) : UserOption {
141
+ return {
142
+ label : user . name ?? user . username ,
143
+ value : user . email ,
144
+ avatar : (
145
+ < UserAvatar size = "xs" username = { user . username } src = { user . avatar_url } />
146
+ ) ,
147
+ } ;
148
+ }
149
+
150
+ function mountOptions (
151
+ users : readonly User [ ] | undefined ,
152
+ selectedUser : User | undefined ,
153
+ ) : UserOption [ ] | undefined {
154
+ if ( ! users ) {
155
+ return undefined ;
156
+ }
157
+
158
+ let usersToDisplay = [ ...users ] ;
159
+
160
+ if ( selectedUser ) {
161
+ const usersIncludeSelectedUser = users . some (
162
+ ( u ) => u . id === selectedUser . id ,
163
+ ) ;
164
+ if ( ! usersIncludeSelectedUser ) {
165
+ usersToDisplay = [ selectedUser , ...usersToDisplay ] ;
166
+ }
167
+ }
168
+
169
+ return usersToDisplay . map ( optionFromUser ) ;
170
+ }
0 commit comments