1
- import { Typography , Tooltip , Button } from '@material-ui/core' ;
2
- import React , { useCallback , useState } from 'react' ;
1
+ import { Avatar , Grid , TextField , Typography , makeStyles } from '@material-ui/core' ;
2
+ import { Autocomplete } from '@material-ui/lab' ;
3
+ import AddIcon from '@material-ui/icons/Add' ;
4
+ import AwesomeDebouncePromise from 'awesome-debounce-promise' ;
5
+ import React , { useCallback , useContext , useState } from 'react' ;
6
+ import { useAsyncAbortable } from 'react-async-hook' ;
7
+ import useConstant from 'use-constant' ;
3
8
import { BitbucketSite , User } from '../../../bitbucket/model' ;
4
- import DialogUserPicker from './DialogUserPicker' ;
9
+ import { PullRequestDetailsControllerContext } from './pullRequestDetailsController' ;
10
+
11
+ const useStyles = makeStyles ( {
12
+ container : {
13
+ display : 'flex' ,
14
+ alignItems : 'center' ,
15
+ width : '100%' ,
16
+ position : 'relative' ,
17
+ paddingLeft : 28 ,
18
+ } ,
19
+ addIcon : {
20
+ padding : 0 ,
21
+ fontSize : 20 ,
22
+ color : 'var(--vscode-editor-foreground)' ,
23
+ opacity : 0.8 ,
24
+ position : 'absolute' ,
25
+ left : 4 ,
26
+ top : '50%' ,
27
+ transform : 'translateY(-50%)' ,
28
+ display : 'flex' ,
29
+ alignItems : 'center' ,
30
+ justifyContent : 'center' ,
31
+ height : '20px' ,
32
+ width : '20px' ,
33
+ lineHeight : 0 ,
34
+ } ,
35
+ autocompleteRoot : {
36
+ flex : 1 ,
37
+ '& .MuiInputBase-root' : {
38
+ backgroundColor : 'transparent' ,
39
+ color : 'var(--vscode-editor-foreground)' ,
40
+ padding : '0 !important' ,
41
+ minHeight : '24px' ,
42
+ display : 'flex' ,
43
+ alignItems : 'center' ,
44
+ border : '1px solid transparent' ,
45
+ borderRadius : '2px' ,
46
+ '&.Mui-focused' : {
47
+ border : '1px solid var(--vscode-editor-foreground)' ,
48
+ } ,
49
+ '&:hover' : {
50
+ border : '1px solid var(--vscode-editor-foreground)' ,
51
+ } ,
52
+ } ,
53
+ '& .MuiOutlinedInput-notchedOutline' : {
54
+ border : 'none' ,
55
+ } ,
56
+ '& .MuiInputBase-input' : {
57
+ color : 'var(--vscode-editor-foreground)' ,
58
+ height : '24px' ,
59
+ lineHeight : '24px' ,
60
+ padding : '0 7px !important' ,
61
+ '&::placeholder' : {
62
+ color : 'var(--vscode-input-placeholderForeground)' ,
63
+ opacity : 0.8 ,
64
+ fontStyle : 'italic' ,
65
+ } ,
66
+ } ,
67
+ '& .MuiAutocomplete-endAdornment' : {
68
+ display : 'none' ,
69
+ } ,
70
+ '& .MuiFormControl-marginDense' : {
71
+ margin : '0' ,
72
+ } ,
73
+ } ,
74
+ optionContainer : {
75
+ padding : '8px' ,
76
+ } ,
77
+ avatarContainer : {
78
+ marginRight : '8px' ,
79
+ } ,
80
+ optionText : {
81
+ color : 'var(--vscode-editor-foreground)' ,
82
+ } ,
83
+ } ) ;
5
84
6
85
type AddReviewersProps = {
7
86
site : BitbucketSite ;
@@ -10,39 +89,85 @@ type AddReviewersProps = {
10
89
} ;
11
90
12
91
export const AddReviewers : React . FunctionComponent < AddReviewersProps > = ( { site, reviewers, updateReviewers } ) => {
13
- const [ isOpen , setIsOpen ] = useState ( false ) ;
92
+ const controller = useContext ( PullRequestDetailsControllerContext ) ;
93
+ const [ inputText , setInputText ] = useState ( '' ) ;
94
+ const classes = useStyles ( ) ;
95
+
96
+ const debouncedUserFetcher = useConstant ( ( ) =>
97
+ AwesomeDebouncePromise (
98
+ async ( site : BitbucketSite , query : string , abortSignal ?: AbortSignal ) : Promise < User [ ] > => {
99
+ return await controller . fetchUsers ( site , query , abortSignal ) ;
100
+ } ,
101
+ 300 ,
102
+ { leading : false } ,
103
+ ) ,
104
+ ) ;
14
105
15
- const handleUpdateReviewers = useCallback (
16
- async ( newUser : User ) => {
17
- setIsOpen ( false ) ;
18
- const updatedReviewers = [ ... reviewers , newUser ] ;
19
- await updateReviewers ( updatedReviewers ) ;
106
+ const handleInputChange = useCallback (
107
+ ( event : React . ChangeEvent , value : string ) => {
108
+ if ( event ?. type === 'change' ) {
109
+ setInputText ( value ) ;
110
+ }
20
111
} ,
21
- [ reviewers , updateReviewers ] ,
112
+ [ setInputText ] ,
22
113
) ;
23
114
24
- const handleToggleOpen = useCallback ( ( ) => {
25
- setIsOpen ( true ) ;
26
- } , [ setIsOpen ] ) ;
115
+ const fetchUsers = useAsyncAbortable (
116
+ async ( abortSignal ) => {
117
+ if ( inputText . length > 1 && site ) {
118
+ const results = await debouncedUserFetcher ( site , inputText , abortSignal ) ;
119
+ return results . filter ( ( user ) => ! reviewers . some ( ( existing ) => existing . accountId === user . accountId ) ) ;
120
+ }
121
+ return [ ] ;
122
+ } ,
123
+ [ site , inputText , reviewers ] ,
124
+ ) ;
27
125
28
- const handleToggleClosed = useCallback ( ( ) => {
29
- setIsOpen ( false ) ;
30
- } , [ setIsOpen ] ) ;
126
+ const handleUserSelect = useCallback (
127
+ async ( event : React . ChangeEvent , user : User | null ) => {
128
+ if ( user ) {
129
+ const updatedReviewers = [ ...reviewers , user ] ;
130
+ await updateReviewers ( updatedReviewers ) ;
131
+ }
132
+ } ,
133
+ [ reviewers , updateReviewers ] ,
134
+ ) ;
31
135
32
136
return (
33
- < React . Fragment >
34
- < Tooltip title = "Add Reviewers" >
35
- < Button color = { 'primary' } onClick = { handleToggleOpen } value = { 'Add Reviewers' } >
36
- < Typography variant = "button" > Add Reviewers</ Typography >
37
- </ Button >
38
- </ Tooltip >
39
- < DialogUserPicker
40
- site = { site }
41
- users = { reviewers }
42
- onChange = { handleUpdateReviewers }
43
- hidden = { isOpen }
44
- onClose = { handleToggleClosed }
137
+ < div className = { classes . container } >
138
+ < AddIcon className = { classes . addIcon } />
139
+ < Autocomplete
140
+ className = { classes . autocompleteRoot }
141
+ size = "small"
142
+ options = { fetchUsers . result || [ ] }
143
+ getOptionLabel = { ( option ) => option ?. displayName || '' }
144
+ onInputChange = { handleInputChange }
145
+ onChange = { handleUserSelect }
146
+ loading = { fetchUsers . loading }
147
+ renderInput = { ( params ) => (
148
+ < TextField
149
+ { ...params }
150
+ variant = "outlined"
151
+ placeholder = "Add reviewer"
152
+ InputProps = { {
153
+ ...params . InputProps ,
154
+ startAdornment : null ,
155
+ } }
156
+ />
157
+ ) }
158
+ renderOption = { ( option ) => (
159
+ < div className = { classes . optionContainer } >
160
+ < Grid container alignItems = "center" >
161
+ < Grid item className = { classes . avatarContainer } >
162
+ < Avatar src = { option ?. avatarUrl } />
163
+ </ Grid >
164
+ < Grid item >
165
+ < Typography className = { classes . optionText } > { option ?. displayName } </ Typography >
166
+ </ Grid >
167
+ </ Grid >
168
+ </ div >
169
+ ) }
45
170
/>
46
- </ React . Fragment >
171
+ </ div >
47
172
) ;
48
173
} ;
0 commit comments