Merge branch 'master' into 14391-advanced-view-tags
[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     ListItem, ListItemText, ListItemSecondaryAction,
15     ClickAwayListener
16 } from '@material-ui/core';
17 import SearchIcon from '@material-ui/icons/Search';
18 import { RemoveIcon, EditSavedQueryIcon } from '~/components/icon/icon';
19 import { SearchView } from '~/store/search-bar/search-bar-reducer';
20 import { SearchBarBasicView } from '~/views-components/search-bar/search-bar-basic-view';
21 import { SearchBarAdvancedView } from '~/views-components/search-bar/search-bar-advanced-view';
22 import { SearchBarAutocompleteView, SearchBarAutocompleteViewDataProps } from '~/views-components/search-bar/search-bar-autocomplete-view';
23 import { ArvadosTheme } from '~/common/custom-theme';
24 import { SearchBarAdvanceFormData } from '~/models/search-bar';
25
26 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
27
28 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
29     return {
30         container: {
31             position: 'relative',
32             width: '100%',
33             borderRadius: theme.spacing.unit / 2
34         },
35         containerSearchViewOpened: {
36             position: 'relative',
37             width: '100%',
38             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
39         },
40         input: {
41             border: 'none',
42             padding: `0px ${theme.spacing.unit}px`
43         },
44         view: {
45             position: 'absolute',
46             width: '100%',
47             zIndex: 1
48         }
49     };
50 };
51
52 type SearchBarDataProps = {
53     searchValue: string;
54     currentView: string;
55     isPopoverOpen: boolean;
56     savedQueries: SearchBarAdvanceFormData[];
57     tags: any;
58 } & SearchBarAutocompleteViewDataProps;
59
60 interface SearchBarActionProps {
61     onSearch: (value: string) => any;
62     searchDataOnEnter: (value: string) => void;
63     debounce?: number;
64     onSetView: (currentView: string) => void;
65     closeView: () => void;
66     saveRecentQuery: (query: string) => void;
67     loadRecentQueries: () => string[];
68     saveQuery: (data: SearchBarAdvanceFormData) => void;
69     deleteSavedQuery: (id: number) => void;
70     openSearchView: () => void;
71     navigateTo: (uuid: string) => void;
72     editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
73 }
74
75 type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
76
77 interface SearchBarState {
78     value: string;
79 }
80
81 interface RenderRecentQueriesProps {
82     text: string;
83     onSearch: (searchValue: string) => void;
84 }
85
86 export const RenderRecentQueries = (props: RenderRecentQueriesProps) => {
87     return <ListItem button>
88         <ListItemText secondary={props.text} onClick={() => props.onSearch(props.text)} />
89     </ListItem>;
90 };
91
92 interface RenderAutocompleteItemsProps {
93     text: string | JSX.Element;
94     navigateTo: (uuid: string) => void;
95     uuid: string;
96 }
97
98 export const RenderAutocompleteItems = (props: RenderAutocompleteItemsProps) => {
99     return <ListItem button>
100         <ListItemText secondary={props.text} onClick={() => props.navigateTo(props.uuid)} />
101     </ListItem>;
102 };
103
104 interface RenderSavedQueriesProps {
105     text: string;
106     id: number;
107     deleteSavedQuery: (id: number) => void;
108     onSearch: (searchValue: string) => void;
109     editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
110     data: SearchBarAdvanceFormData;
111 }
112
113 export const RenderSavedQueries = (props: RenderSavedQueriesProps) => {
114     return <ListItem button>
115         <ListItemText secondary={props.text} onClick={() => props.onSearch(props.text)} />
116         <ListItemSecondaryAction>
117             <Tooltip title="Edit">
118                 <IconButton aria-label="Edit" onClick={() => props.editSavedQuery(props.data, props.id)}>
119                     <EditSavedQueryIcon />
120                 </IconButton>
121             </Tooltip>
122             <Tooltip title="Remove">
123                 <IconButton aria-label="Remove" onClick={() => props.deleteSavedQuery(props.id)}>
124                     <RemoveIcon />
125                 </IconButton>
126             </Tooltip>
127         </ListItemSecondaryAction>
128     </ListItem>;
129 };
130
131 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
132
133 export const SearchBarView = withStyles(styles)(
134     class extends React.Component<SearchBarProps> {
135         state: SearchBarState = {
136             value: ""
137         };
138
139         timeout: number;
140
141         render() {
142             const { classes, currentView, openSearchView, closeView, isPopoverOpen } = this.props;
143             return <ClickAwayListener onClickAway={closeView}>
144                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
145                     <form onSubmit={this.handleSubmit}>
146                         <Input
147                             className={classes.input}
148                             onChange={this.handleChange}
149                             placeholder="Search"
150                             value={this.state.value}
151                             fullWidth={true}
152                             disableUnderline={true}
153                             onClick={openSearchView}
154                             endAdornment={
155                                 <InputAdornment position="end">
156                                     <Tooltip title='Search'>
157                                         <IconButton>
158                                             <SearchIcon />
159                                         </IconButton>
160                                     </Tooltip>
161                                 </InputAdornment>
162                             } />
163                     </form>
164                     <div className={classes.view}>
165                         {isPopoverOpen && this.getView(currentView)}
166                     </div>
167                 </Paper >
168             </ClickAwayListener>;
169         }
170
171         componentDidMount() {
172             this.setState({ value: this.props.searchValue });
173         }
174
175         componentWillReceiveProps(nextProps: SearchBarProps) {
176             if (nextProps.searchValue !== this.props.searchValue) {
177                 this.setState({ value: nextProps.searchValue });
178             }
179         }
180
181         componentWillUnmount() {
182             clearTimeout(this.timeout);
183         }
184
185         getView = (currentView: string) => {
186             const { onSetView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue, searchResults, saveQuery, onSearch, navigateTo, editSavedQuery, tags } = this.props;
187             switch (currentView) {
188                 case SearchView.BASIC:
189                     return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} onSearch={onSearch} editSavedQuery={editSavedQuery} />;
190                 case SearchView.ADVANCED:
191                     return <SearchBarAdvancedView setView={onSetView} saveQuery={saveQuery} tags={tags} />;
192                 case SearchView.AUTOCOMPLETE:
193                     return <SearchBarAutocompleteView
194                         navigateTo={navigateTo}
195                         searchResults={searchResults}
196                         searchValue={searchValue} />;
197                 default:
198                     return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} onSearch={onSearch} editSavedQuery={editSavedQuery} />;
199             }
200         }
201
202         handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
203             event.preventDefault();
204             clearTimeout(this.timeout);
205             this.props.saveRecentQuery(this.state.value);
206             this.props.searchDataOnEnter(this.state.value);
207             this.props.loadRecentQueries();
208         }
209
210         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
211             clearTimeout(this.timeout);
212             this.setState({ value: event.target.value });
213             this.timeout = window.setTimeout(
214                 () => this.props.onSearch(this.state.value),
215                 this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
216             );
217             if (event.target.value.length > 0) {
218                 this.props.onSetView(SearchView.AUTOCOMPLETE);
219             } else {
220                 this.props.onSetView(SearchView.BASIC);
221             }
222         }
223     }
224 );