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