Merge branch '14452-fix-typo'
[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
36 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
37
38 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
39     return {
40         container: {
41             position: 'relative',
42             width: '100%',
43             borderRadius: theme.spacing.unit / 2,
44             zIndex: theme.zIndex.modal,
45         },
46         containerSearchViewOpened: {
47             position: 'relative',
48             width: '100%',
49             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
50             zIndex: theme.zIndex.modal,
51         },
52         input: {
53             border: 'none',
54             padding: `0`
55         },
56         view: {
57             position: 'absolute',
58             width: '100%',
59             zIndex: 1
60         }
61     };
62 };
63
64 export type SearchBarDataProps = SearchBarViewDataProps
65     & SearchBarAutocompleteViewDataProps
66     & SearchBarAdvancedViewDataProps
67     & SearchBarBasicViewDataProps;
68
69 interface SearchBarViewDataProps {
70     searchValue: string;
71     currentView: string;
72     isPopoverOpen: boolean;
73     debounce?: number;
74 }
75
76 export type SearchBarActionProps = SearchBarViewActionProps
77     & SearchBarAutocompleteViewActionProps
78     & SearchBarAdvancedViewActionProps
79     & SearchBarBasicViewActionProps;
80
81 interface SearchBarViewActionProps {
82     onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
83     onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
84     onSetView: (currentView: string) => void;
85     closeView: () => void;
86     openSearchView: () => void;
87     loadRecentQueries: () => string[];
88     moveUp: () => void;
89     moveDown: () => void;
90     setAdvancedDataFromSearchValue: (search: string) => void;
91 }
92
93 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
94
95 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
96     if (e.keyCode === KEY_CODE_DOWN) {
97         e.preventDefault();
98         if (!props.isPopoverOpen) {
99             props.onSetView(SearchView.AUTOCOMPLETE);
100             props.openSearchView();
101         } else {
102             props.moveDown();
103         }
104     } else if (e.keyCode === KEY_CODE_UP) {
105         e.preventDefault();
106         props.moveUp();
107     } else if (e.keyCode === KEY_CODE_ESC) {
108         e.preventDefault();
109         props.closeView();
110     } else if (e.keyCode === KEY_ENTER) {
111         if (props.currentView === SearchView.BASIC) {
112             e.preventDefault();
113             props.onSearch(props.selectedItem.query);
114         } else if (props.currentView === SearchView.AUTOCOMPLETE) {
115             if (props.selectedItem.id !== props.searchValue) {
116                 e.preventDefault();
117                 props.navigateTo(props.selectedItem.id);
118             }
119         }
120     }
121 };
122
123 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
124     if (props.searchValue) {
125         props.onSetView(SearchView.AUTOCOMPLETE);
126         props.openSearchView();
127     } else {
128         props.closeView();
129     }
130 };
131
132 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
133     e.stopPropagation();
134     if (props.isPopoverOpen) {
135         if (props.currentView === SearchView.ADVANCED) {
136             props.closeView();
137         } else {
138             props.setAdvancedDataFromSearchValue(props.searchValue);
139             props.onSetView(SearchView.ADVANCED);
140         }
141     } else {
142         props.setAdvancedDataFromSearchValue(props.searchValue);
143         props.onSetView(SearchView.ADVANCED);
144     }
145 };
146
147 export const SearchBarView = withStyles(styles)(
148     (props: SearchBarViewProps) => {
149         const { classes, isPopoverOpen } = props;
150         return (
151             <>
152
153                 {isPopoverOpen &&
154                     <Backdrop onClick={props.closeView} />}
155
156                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
157                     <form onSubmit={props.onSubmit}>
158                         <Input
159                             className={classes.input}
160                             onChange={props.onChange}
161                             placeholder="Search"
162                             value={props.searchValue}
163                             fullWidth={true}
164                             disableUnderline={true}
165                             onClick={e => handleInputClick(e, props)}
166                             onKeyDown={e => handleKeyDown(e, props)}
167                             startAdornment={
168                                 <InputAdornment position="start">
169                                     <Tooltip title='Search'>
170                                         <IconButton type="submit">
171                                             <SearchIcon />
172                                         </IconButton>
173                                     </Tooltip>
174                                 </InputAdornment>
175                             }
176                             endAdornment={
177                                 <InputAdornment position="end">
178                                     <Tooltip title='Advanced search'>
179                                         <IconButton onClick={e => handleDropdownClick(e, props)}>
180                                             <ArrowDropDownIcon />
181                                         </IconButton>
182                                     </Tooltip>
183                                 </InputAdornment>
184                             } />
185                     </form>
186                     <div className={classes.view}>
187                         {isPopoverOpen && getView({ ...props })}
188                     </div>
189                 </Paper >
190             </>
191         );
192     }
193 );
194
195 const getView = (props: SearchBarViewProps) => {
196     switch (props.currentView) {
197         case SearchView.AUTOCOMPLETE:
198             return <SearchBarAutocompleteView
199                 navigateTo={props.navigateTo}
200                 searchResults={props.searchResults}
201                 searchValue={props.searchValue}
202                 selectedItem={props.selectedItem} />;
203         case SearchView.ADVANCED:
204             return <SearchBarAdvancedView
205                 closeAdvanceView={props.closeAdvanceView}
206                 tags={props.tags}
207                 saveQuery={props.saveQuery} />;
208         default:
209             return <SearchBarBasicView
210                 onSetView={props.onSetView}
211                 onSearch={props.onSearch}
212                 loadRecentQueries={props.loadRecentQueries}
213                 savedQueries={props.savedQueries}
214                 deleteSavedQuery={props.deleteSavedQuery}
215                 editSavedQuery={props.editSavedQuery}
216                 selectedItem={props.selectedItem} />;
217     }
218 };
219
220 const Backdrop = withStyles<'backdrop'>(theme => ({
221     backdrop: {
222         position: 'fixed',
223         top: 0,
224         right: 0,
225         bottom: 0,
226         left: 0,
227         zIndex: theme.zIndex.modal
228     }
229 }))(
230     ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps<HTMLDivElement>) =>
231         <div className={classes.backdrop} {...props} />);