Add query <-> advanced data synchronization
[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 }
90
91 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
92
93 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
94     if (e.keyCode === KEY_CODE_DOWN) {
95         e.preventDefault();
96         if (!props.isPopoverOpen) {
97             props.openSearchView();
98         } else {
99             props.moveDown();
100         }
101     } else if (e.keyCode === KEY_CODE_UP) {
102         e.preventDefault();
103         props.moveUp();
104     } else if (e.keyCode === KEY_CODE_ESC) {
105         e.preventDefault();
106         props.closeView();
107     } else if (e.keyCode === KEY_ENTER) {
108         if (props.currentView === SearchView.BASIC) {
109             e.preventDefault();
110             props.onSearch(props.selectedItem.query);
111         } else if (props.currentView === SearchView.AUTOCOMPLETE) {
112             if (props.selectedItem.id !== props.searchValue) {
113                 e.preventDefault();
114                 props.navigateTo(props.selectedItem.id);
115             }
116         }
117     }
118 };
119
120 export const SearchBarView = withStyles(styles)(
121     (props : SearchBarViewProps) => {
122         const { classes, isPopoverOpen } = props;
123         return (
124             <ClickAwayListener onClickAway={props.closeView}>
125                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
126                     <form onSubmit={props.onSubmit}>
127                         <Input
128                             className={classes.input}
129                             onChange={props.onChange}
130                             placeholder="Search"
131                             value={props.searchValue}
132                             fullWidth={true}
133                             disableUnderline={true}
134                             onClick={props.openSearchView}
135                             onKeyDown={e => handleKeyDown(e, props)}
136                             startAdornment={
137                                 <InputAdornment position="start">
138                                     <Tooltip title='Search'>
139                                         <IconButton type="submit">
140                                             <SearchIcon />
141                                         </IconButton>
142                                     </Tooltip>
143                                 </InputAdornment>
144                             }
145                             endAdornment={
146                                 <InputAdornment position="end">
147                                     <Tooltip title='Advanced search'>
148                                         <IconButton onClick={() => props.onSetView(SearchView.ADVANCED)}>
149                                             <ArrowDropDownIcon />
150                                         </IconButton>
151                                     </Tooltip>
152                                 </InputAdornment>
153                             } />
154                     </form>
155                     <div className={classes.view}>
156                         {isPopoverOpen && getView({...props})}
157                     </div>
158                 </Paper >
159             </ClickAwayListener>
160         );
161     }
162 );
163
164 const getView = (props: SearchBarViewProps) => {
165     switch (props.currentView) {
166         case SearchView.AUTOCOMPLETE:
167             return <SearchBarAutocompleteView
168                 navigateTo={props.navigateTo}
169                 searchResults={props.searchResults}
170                 searchValue={props.searchValue}
171                 selectedItem={props.selectedItem} />;
172         case SearchView.ADVANCED:
173             return <SearchBarAdvancedView
174                 closeAdvanceView={props.closeAdvanceView}
175                 tags={props.tags} />;
176         default:
177             return <SearchBarBasicView
178                 onSetView={props.onSetView}
179                 onSearch={props.onSearch}
180                 loadRecentQueries={props.loadRecentQueries}
181                 savedQueries={props.savedQueries}
182                 deleteSavedQuery={props.deleteSavedQuery}
183                 editSavedQuery={props.editSavedQuery}
184                 selectedItem={props.selectedItem} />;
185     }
186 };