Move search call debounce to component
[arvados-workbench2.git] / src / views-components / search-bar / search-bar-view.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import {
7     IconButton,
8     Paper,
9     StyleRulesCallback,
10     withStyles,
11     WithStyles,
12     Tooltip,
13     InputAdornment, Input,
14 } from '@material-ui/core';
15 import SearchIcon from '@material-ui/icons/Search';
16 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
17 import { ArvadosTheme } from '~/common/custom-theme';
18 import { SearchView } from '~/store/search-bar/search-bar-reducer';
19 import {
20     SearchBarBasicView,
21     SearchBarBasicViewDataProps,
22     SearchBarBasicViewActionProps
23 } from '~/views-components/search-bar/search-bar-basic-view';
24 import {
25     SearchBarAutocompleteView,
26     SearchBarAutocompleteViewDataProps,
27     SearchBarAutocompleteViewActionProps
28 } from '~/views-components/search-bar/search-bar-autocomplete-view';
29 import {
30     SearchBarAdvancedView,
31     SearchBarAdvancedViewDataProps,
32     SearchBarAdvancedViewActionProps
33 } from '~/views-components/search-bar/search-bar-advanced-view';
34 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
35 import { debounce } from 'debounce';
36
37 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
38
39 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
40     return {
41         container: {
42             position: 'relative',
43             width: '100%',
44             borderRadius: theme.spacing.unit / 2,
45             zIndex: theme.zIndex.modal,
46         },
47         containerSearchViewOpened: {
48             position: 'relative',
49             width: '100%',
50             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
51             zIndex: theme.zIndex.modal,
52         },
53         input: {
54             border: 'none',
55             padding: `0`
56         },
57         view: {
58             position: 'absolute',
59             width: '100%',
60             zIndex: 1
61         }
62     };
63 };
64
65 export type SearchBarDataProps = SearchBarViewDataProps
66     & SearchBarAutocompleteViewDataProps
67     & SearchBarAdvancedViewDataProps
68     & SearchBarBasicViewDataProps;
69
70 interface SearchBarViewDataProps {
71     searchValue: string;
72     currentView: string;
73     isPopoverOpen: boolean;
74     debounce?: number;
75 }
76
77 export type SearchBarActionProps = SearchBarViewActionProps
78     & SearchBarAutocompleteViewActionProps
79     & SearchBarAdvancedViewActionProps
80     & SearchBarBasicViewActionProps;
81
82 interface SearchBarViewActionProps {
83     onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
84     onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
85     onSetView: (currentView: string) => void;
86     closeView: () => void;
87     openSearchView: () => void;
88     loadRecentQueries: () => string[];
89     moveUp: () => void;
90     moveDown: () => void;
91     setAdvancedDataFromSearchValue: (search: string) => void;
92 }
93
94 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
95
96 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
97     if (e.keyCode === KEY_CODE_DOWN) {
98         e.preventDefault();
99         if (!props.isPopoverOpen) {
100             props.onSetView(SearchView.AUTOCOMPLETE);
101             props.openSearchView();
102         } else {
103             props.moveDown();
104         }
105     } else if (e.keyCode === KEY_CODE_UP) {
106         e.preventDefault();
107         props.moveUp();
108     } else if (e.keyCode === KEY_CODE_ESC) {
109         e.preventDefault();
110         props.closeView();
111     } else if (e.keyCode === KEY_ENTER) {
112         if (props.currentView === SearchView.BASIC) {
113             e.preventDefault();
114             props.onSearch(props.selectedItem.query);
115         } else if (props.currentView === SearchView.AUTOCOMPLETE) {
116             if (props.selectedItem.id !== props.searchValue) {
117                 e.preventDefault();
118                 props.navigateTo(props.selectedItem.id);
119             }
120         }
121     }
122 };
123
124 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
125     if (props.searchValue) {
126         props.onSetView(SearchView.AUTOCOMPLETE);
127         props.openSearchView();
128     } else {
129         props.closeView();
130     }
131 };
132
133 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
134     e.stopPropagation();
135     if (props.isPopoverOpen) {
136         if (props.currentView === SearchView.ADVANCED) {
137             props.closeView();
138         } else {
139             props.setAdvancedDataFromSearchValue(props.searchValue);
140             props.onSetView(SearchView.ADVANCED);
141         }
142     } else {
143         props.setAdvancedDataFromSearchValue(props.searchValue);
144         props.onSetView(SearchView.ADVANCED);
145     }
146 };
147
148 export const SearchBarView = withStyles(styles)(
149     class extends React.Component<SearchBarViewProps> {
150
151         debouncedSearch = debounce(() => {
152             this.props.onSearch(this.props.searchValue);
153         }, 1000);
154
155         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
156             this.debouncedSearch();
157             this.props.onChange(event);
158         }
159
160         handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
161             this.debouncedSearch.clear();
162             this.props.onSubmit(event);
163         }
164
165         componentWillUnmount() {
166             this.debouncedSearch.clear();
167         }
168
169         render() {
170             const { children, ...props } = this.props;
171             const { classes, isPopoverOpen } = this.props;
172             return (
173                 <>
174
175                     {isPopoverOpen &&
176                         <Backdrop onClick={props.closeView} />}
177
178                     <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
179                         <form onSubmit={this.handleSubmit}>
180                             <Input
181                                 className={classes.input}
182                                 onChange={this.handleChange}
183                                 placeholder="Search"
184                                 value={props.searchValue}
185                                 fullWidth={true}
186                                 disableUnderline={true}
187                                 onClick={e => handleInputClick(e, props)}
188                                 onKeyDown={e => handleKeyDown(e, props)}
189                                 startAdornment={
190                                     <InputAdornment position="start">
191                                         <Tooltip title='Search'>
192                                             <IconButton type="submit">
193                                                 <SearchIcon />
194                                             </IconButton>
195                                         </Tooltip>
196                                     </InputAdornment>
197                                 }
198                                 endAdornment={
199                                     <InputAdornment position="end">
200                                         <Tooltip title='Advanced search'>
201                                             <IconButton onClick={e => handleDropdownClick(e, props)}>
202                                                 <ArrowDropDownIcon />
203                                             </IconButton>
204                                         </Tooltip>
205                                     </InputAdornment>
206                                 } />
207                         </form>
208                         <div className={classes.view}>
209                             {isPopoverOpen && getView({ ...props })}
210                         </div>
211                     </Paper >
212                 </>
213             );
214         }
215     });
216
217 const getView = (props: SearchBarViewProps) => {
218     switch (props.currentView) {
219         case SearchView.AUTOCOMPLETE:
220             return <SearchBarAutocompleteView
221                 navigateTo={props.navigateTo}
222                 searchResults={props.searchResults}
223                 searchValue={props.searchValue}
224                 selectedItem={props.selectedItem} />;
225         case SearchView.ADVANCED:
226             return <SearchBarAdvancedView
227                 closeAdvanceView={props.closeAdvanceView}
228                 tags={props.tags}
229                 saveQuery={props.saveQuery} />;
230         default:
231             return <SearchBarBasicView
232                 onSetView={props.onSetView}
233                 onSearch={props.onSearch}
234                 loadRecentQueries={props.loadRecentQueries}
235                 savedQueries={props.savedQueries}
236                 deleteSavedQuery={props.deleteSavedQuery}
237                 editSavedQuery={props.editSavedQuery}
238                 selectedItem={props.selectedItem} />;
239     }
240 };
241
242 const Backdrop = withStyles<'backdrop'>(theme => ({
243     backdrop: {
244         position: 'fixed',
245         top: 0,
246         right: 0,
247         bottom: 0,
248         left: 0,
249         zIndex: theme.zIndex.modal
250     }
251 }))(
252     ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps<HTMLDivElement>) =>
253         <div className={classes.backdrop} {...props} />);