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