15069: Simplifies conditional logic.
[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 && props.currentView === SearchView.ADVANCED) {
136         props.closeView();
137     } else {
138         props.setAdvancedDataFromSearchValue(props.searchValue);
139         props.onSetView(SearchView.ADVANCED);
140     }
141 };
142
143 export const SearchBarView = withStyles(styles)(
144     class extends React.Component<SearchBarViewProps> {
145
146         debouncedSearch = debounce(() => {
147             this.props.onSearch(this.props.searchValue);
148         }, 1000);
149
150         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
151             this.debouncedSearch();
152             this.props.onChange(event);
153         }
154
155         handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
156             this.debouncedSearch.clear();
157             this.props.onSubmit(event);
158         }
159
160         componentWillUnmount() {
161             this.debouncedSearch.clear();
162         }
163
164         render() {
165             const { children, ...props } = this.props;
166             const { classes, isPopoverOpen } = this.props;
167             return (
168                 <>
169
170                     {isPopoverOpen &&
171                         <Backdrop onClick={props.closeView} />}
172
173                     <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
174                         <form onSubmit={this.handleSubmit}>
175                             <Input
176                                 className={classes.input}
177                                 onChange={this.handleChange}
178                                 placeholder="Search"
179                                 value={props.searchValue}
180                                 fullWidth={true}
181                                 disableUnderline={true}
182                                 onClick={e => handleInputClick(e, props)}
183                                 onKeyDown={e => handleKeyDown(e, props)}
184                                 startAdornment={
185                                     <InputAdornment position="start">
186                                         <Tooltip title='Search'>
187                                             <IconButton type="submit">
188                                                 <SearchIcon />
189                                             </IconButton>
190                                         </Tooltip>
191                                     </InputAdornment>
192                                 }
193                                 endAdornment={
194                                     <InputAdornment position="end">
195                                         <Tooltip title='Advanced search'>
196                                             <IconButton onClick={e => handleDropdownClick(e, props)}>
197                                                 <ArrowDropDownIcon />
198                                             </IconButton>
199                                         </Tooltip>
200                                     </InputAdornment>
201                                 } />
202                         </form>
203                         <div className={classes.view}>
204                             {isPopoverOpen && getView({ ...props })}
205                         </div>
206                     </Paper >
207                 </>
208             );
209         }
210     });
211
212 const getView = (props: SearchBarViewProps) => {
213     switch (props.currentView) {
214         case SearchView.AUTOCOMPLETE:
215             return <SearchBarAutocompleteView
216                 navigateTo={props.navigateTo}
217                 searchResults={props.searchResults}
218                 searchValue={props.searchValue}
219                 selectedItem={props.selectedItem} />;
220         case SearchView.ADVANCED:
221             return <SearchBarAdvancedView
222                 closeAdvanceView={props.closeAdvanceView}
223                 tags={props.tags}
224                 saveQuery={props.saveQuery} />;
225         default:
226             return <SearchBarBasicView
227                 onSetView={props.onSetView}
228                 onSearch={props.onSearch}
229                 loadRecentQueries={props.loadRecentQueries}
230                 savedQueries={props.savedQueries}
231                 deleteSavedQuery={props.deleteSavedQuery}
232                 editSavedQuery={props.editSavedQuery}
233                 selectedItem={props.selectedItem} />;
234     }
235 };
236
237 const Backdrop = withStyles<'backdrop'>(theme => ({
238     backdrop: {
239         position: 'fixed',
240         top: 0,
241         right: 0,
242         bottom: 0,
243         left: 0,
244         zIndex: theme.zIndex.modal
245     }
246 }))(
247     ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps<HTMLDivElement>) =>
248         <div className={classes.backdrop} {...props} />);