Merge branch '14434-display-workflow-name'
[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     Popover,
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     class SearchBarView extends React.Component<SearchBarViewProps> {
148
149         viewAnchorRef = React.createRef<HTMLDivElement>();
150
151         render() {
152             const { children, ...props } = this.props;
153             const { classes, isPopoverOpen } = props;
154             return (
155                 <Paper
156                     className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
157                     <div ref={this.viewAnchorRef}>
158                         <form onSubmit={props.onSubmit}>
159                             <SearchInput {...props} />
160                         </form>
161                     </div>
162                     <SearchViewContainer
163                         {...props}
164                         width={this.getViewWidth()}
165                         anchorEl={this.viewAnchorRef.current}>
166                         {
167                             getView({ ...props })
168                         }
169                     </SearchViewContainer>
170                 </Paper >
171             );
172         }
173
174         getViewWidth() {
175             const { current } = this.viewAnchorRef;
176             return current ? current.offsetWidth : 0;
177         }
178     }
179
180 );
181
182 const SearchInput = (props: SearchBarViewProps) => {
183     const { classes } = props;
184     return <Input
185         className={classes.input}
186         onChange={props.onChange}
187         placeholder="Search"
188         value={props.searchValue}
189         fullWidth={true}
190         disableUnderline={true}
191         onClick={e => handleInputClick(e, props)}
192         onKeyDown={e => handleKeyDown(e, props)}
193         startAdornment={
194             <InputAdornment position="start">
195                 <Tooltip title='Search'>
196                     <IconButton type="submit">
197                         <SearchIcon />
198                     </IconButton>
199                 </Tooltip>
200             </InputAdornment>
201         }
202         endAdornment={
203             <InputAdornment position="end">
204                 <Tooltip title='Advanced search'>
205                     <IconButton onClick={e => handleDropdownClick(e, props)}>
206                         <ArrowDropDownIcon />
207                     </IconButton>
208                 </Tooltip>
209             </InputAdornment>
210         } />;
211 };
212
213 const SearchViewContainer = (props: SearchBarViewProps & { width: number, anchorEl: HTMLElement | null, children: React.ReactNode }) =>
214     <Popover
215         PaperProps={{
216             style: {
217                 width: props.width,
218                 borderRadius: '0 0 4px 4px',
219             }
220         }}
221         anchorEl={props.anchorEl}
222         anchorOrigin={{
223             vertical: 'bottom',
224             horizontal: 'center',
225         }}
226         transformOrigin={{
227             vertical: 'top',
228             horizontal: 'center',
229         }}
230         disableAutoFocus
231         open={props.isPopoverOpen}
232         onClose={props.closeView}>
233         {
234             props.children
235         }
236     </Popover>;
237
238
239 const getView = (props: SearchBarViewProps) => {
240     switch (props.currentView) {
241         case SearchView.AUTOCOMPLETE:
242             return <SearchBarAutocompleteView
243                 navigateTo={props.navigateTo}
244                 searchResults={props.searchResults}
245                 searchValue={props.searchValue}
246                 selectedItem={props.selectedItem} />;
247         case SearchView.ADVANCED:
248             return <SearchBarAdvancedView
249                 closeAdvanceView={props.closeAdvanceView}
250                 tags={props.tags}
251                 saveQuery={props.saveQuery} />;
252         default:
253             return <SearchBarBasicView
254                 onSetView={props.onSetView}
255                 onSearch={props.onSearch}
256                 loadRecentQueries={props.loadRecentQueries}
257                 savedQueries={props.savedQueries}
258                 deleteSavedQuery={props.deleteSavedQuery}
259                 editSavedQuery={props.editSavedQuery}
260                 selectedItem={props.selectedItem} />;
261     }
262 };