Merge branch '14452-my-account'
[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     ClickAwayListener
15 } from '@material-ui/core';
16 import SearchIcon from '@material-ui/icons/Search';
17 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
18 import { ArvadosTheme } from '~/common/custom-theme';
19 import { SearchView } from '~/store/search-bar/search-bar-reducer';
20 import {
21     SearchBarBasicView,
22     SearchBarBasicViewDataProps,
23     SearchBarBasicViewActionProps
24 } from '~/views-components/search-bar/search-bar-basic-view';
25 import {
26     SearchBarAutocompleteView,
27     SearchBarAutocompleteViewDataProps,
28     SearchBarAutocompleteViewActionProps
29 } from '~/views-components/search-bar/search-bar-autocomplete-view';
30 import {
31     SearchBarAdvancedView,
32     SearchBarAdvancedViewDataProps,
33     SearchBarAdvancedViewActionProps
34 } from '~/views-components/search-bar/search-bar-advanced-view';
35 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
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         },
46         containerSearchViewOpened: {
47             position: 'relative',
48             width: '100%',
49             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
50         },
51         input: {
52             border: 'none',
53             padding: `0`
54         },
55         view: {
56             position: 'absolute',
57             width: '100%',
58             zIndex: 1
59         }
60     };
61 };
62
63 export type SearchBarDataProps = SearchBarViewDataProps
64     & SearchBarAutocompleteViewDataProps
65     & SearchBarAdvancedViewDataProps
66     & SearchBarBasicViewDataProps;
67
68 interface SearchBarViewDataProps {
69     searchValue: string;
70     currentView: string;
71     isPopoverOpen: boolean;
72     debounce?: number;
73 }
74
75 export type SearchBarActionProps = SearchBarViewActionProps
76     & SearchBarAutocompleteViewActionProps
77     & SearchBarAdvancedViewActionProps
78     & SearchBarBasicViewActionProps;
79
80 interface SearchBarViewActionProps {
81     onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
82     onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
83     onSetView: (currentView: string) => void;
84     closeView: () => void;
85     openSearchView: () => void;
86     loadRecentQueries: () => string[];
87     moveUp: () => void;
88     moveDown: () => void;
89     setAdvancedDataFromSearchValue: (search: string) => void;
90 }
91
92 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
93
94 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
95     if (e.keyCode === KEY_CODE_DOWN) {
96         e.preventDefault();
97         if (!props.isPopoverOpen) {
98             props.onSetView(SearchView.AUTOCOMPLETE);
99             props.openSearchView();
100         } else {
101             props.moveDown();
102         }
103     } else if (e.keyCode === KEY_CODE_UP) {
104         e.preventDefault();
105         props.moveUp();
106     } else if (e.keyCode === KEY_CODE_ESC) {
107         e.preventDefault();
108         props.closeView();
109     } else if (e.keyCode === KEY_ENTER) {
110         if (props.currentView === SearchView.BASIC) {
111             e.preventDefault();
112             props.onSearch(props.selectedItem.query);
113         } else if (props.currentView === SearchView.AUTOCOMPLETE) {
114             if (props.selectedItem.id !== props.searchValue) {
115                 e.preventDefault();
116                 props.navigateTo(props.selectedItem.id);
117             }
118         }
119     }
120 };
121
122 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
123     if (props.searchValue) {
124         props.onSetView(SearchView.AUTOCOMPLETE);
125         props.openSearchView();
126     } else {
127         props.closeView();
128     }
129 };
130
131 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
132     e.stopPropagation();
133     if (props.isPopoverOpen) {
134         if (props.currentView === SearchView.ADVANCED) {
135             props.closeView();
136         } else {
137             props.setAdvancedDataFromSearchValue(props.searchValue);
138             props.onSetView(SearchView.ADVANCED);
139         }
140     } else {
141         props.setAdvancedDataFromSearchValue(props.searchValue);
142         props.onSetView(SearchView.ADVANCED);
143     }
144 };
145
146 export const SearchBarView = withStyles(styles)(
147     (props : SearchBarViewProps) => {
148         const { classes, isPopoverOpen } = props;
149         return (
150             <ClickAwayListener onClickAway={props.closeView}>
151                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
152                     <form onSubmit={props.onSubmit}>
153                         <Input
154                             className={classes.input}
155                             onChange={props.onChange}
156                             placeholder="Search"
157                             value={props.searchValue}
158                             fullWidth={true}
159                             disableUnderline={true}
160                             onClick={e => handleInputClick(e, props)}
161                             onKeyDown={e => handleKeyDown(e, props)}
162                             startAdornment={
163                                 <InputAdornment position="start">
164                                     <Tooltip title='Search'>
165                                         <IconButton type="submit">
166                                             <SearchIcon />
167                                         </IconButton>
168                                     </Tooltip>
169                                 </InputAdornment>
170                             }
171                             endAdornment={
172                                 <InputAdornment position="end">
173                                     <Tooltip title='Advanced search'>
174                                         <IconButton onClick={e => handleDropdownClick(e, props)}>
175                                             <ArrowDropDownIcon />
176                                         </IconButton>
177                                     </Tooltip>
178                                 </InputAdornment>
179                             } />
180                     </form>
181                     <div className={classes.view}>
182                         {isPopoverOpen && getView({...props})}
183                     </div>
184                 </Paper >
185             </ClickAwayListener>
186         );
187     }
188 );
189
190 const getView = (props: SearchBarViewProps) => {
191     switch (props.currentView) {
192         case SearchView.AUTOCOMPLETE:
193             return <SearchBarAutocompleteView
194                 navigateTo={props.navigateTo}
195                 searchResults={props.searchResults}
196                 searchValue={props.searchValue}
197                 selectedItem={props.selectedItem} />;
198         case SearchView.ADVANCED:
199             return <SearchBarAdvancedView
200                 closeAdvanceView={props.closeAdvanceView}
201                 tags={props.tags}
202                 saveQuery={props.saveQuery} />;
203         default:
204             return <SearchBarBasicView
205                 onSetView={props.onSetView}
206                 onSearch={props.onSearch}
207                 loadRecentQueries={props.loadRecentQueries}
208                 savedQueries={props.savedQueries}
209                 deleteSavedQuery={props.deleteSavedQuery}
210                 editSavedQuery={props.editSavedQuery}
211                 selectedItem={props.selectedItem} />;
212     }
213 };