Merge branch '14313-basic-view-recent-queries'
[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 } 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
25 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'searchBar';
26
27 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
28     return {
29         container: {
30             position: 'relative',
31             width: '100%',
32             borderRadius: theme.spacing.unit / 4
33         },
34         containerSearchViewOpened: {
35             position: 'relative',
36             width: '100%',
37             borderRadius: `${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px 0 0`
38         },
39         input: {
40             border: 'none',
41             padding: `0px ${theme.spacing.unit}px`
42         },
43         searchBar: {
44             height: '30px'
45         }
46     };
47 };
48
49 type SearchBarDataProps = {
50     searchValue: string;
51     currentView: string;
52     open: boolean;
53 } & SearchBarAutocompleteViewDataProps;
54
55 interface SearchBarActionProps {
56     onSearch: (value: string) => any;
57     debounce?: number;
58     onSetView: (currentView: string) => void;
59     openView: () => void;
60     closeView: () => void;
61     saveQuery: (query: string) => void;
62     loadQueries: () => string[];
63 }
64
65 type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
66
67 interface SearchBarState {
68     value: string;
69 }
70
71 interface RenderQueriesProps {
72     text: string | JSX.Element;
73 }
74
75 export const RecentQueriesItem = (props: RenderQueriesProps) => {
76     return <ListItem button>
77         <ListItemText secondary={props.text} />
78     </ListItem>;
79 };
80
81
82 export const RenderSavedQueries = (props: RenderQueriesProps) => {
83     return <ListItem button>
84         <ListItemText secondary={props.text} />
85         <ListItemSecondaryAction>
86             <Tooltip title="Remove">
87                 <IconButton aria-label="Remove">
88                     <RemoveIcon />
89                 </IconButton>
90             </Tooltip>
91         </ListItemSecondaryAction>
92     </ListItem>;
93 };
94
95 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
96
97 export const SearchBarView = withStyles(styles)(
98     class extends React.Component<SearchBarProps> {
99         state: SearchBarState = {
100             value: ""
101         };
102
103         timeout: number;
104
105         render() {
106             const { classes, currentView, openView, closeView, open } = this.props;
107             return <ClickAwayListener onClickAway={() => closeView()}>
108                 <Paper className={open ? classes.containerSearchViewOpened : classes.container} >
109                     <form onSubmit={this.handleSubmit} className={classes.searchBar}>
110                         <Input
111                             className={classes.input}
112                             onChange={this.handleChange}
113                             placeholder="Search"
114                             value={this.state.value}
115                             fullWidth={true}
116                             disableUnderline={true}
117                             onClick={() => openView()}
118                             endAdornment={
119                                 <InputAdornment position="end">
120                                     <Tooltip title='Search'>
121                                         <IconButton>
122                                             <SearchIcon />
123                                         </IconButton>
124                                     </Tooltip>
125                                 </InputAdornment>
126                             } />
127                         {open && this.getView(currentView)}
128                     </form>
129                 </Paper >
130             </ClickAwayListener>;
131         }
132
133         componentDidMount() {
134             this.setState({ value: this.props.searchValue });
135         }
136
137         componentWillReceiveProps(nextProps: SearchBarProps) {
138             if (nextProps.searchValue !== this.props.searchValue) {
139                 this.setState({ value: nextProps.searchValue });
140             }
141         }
142
143         componentWillUnmount() {
144             clearTimeout(this.timeout);
145         }
146
147         getView = (currentView: string) => {
148             switch (currentView) {
149                 case SearchView.BASIC:
150                     return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
151                 case SearchView.ADVANCED:
152                     return <SearchBarAdvancedView setView={this.props.onSetView} />;
153                 case SearchView.AUTOCOMPLETE:
154                     return <SearchBarAutocompleteView 
155                                 searchResults={this.props.searchResults} 
156                                 searchValue={this.props.searchValue} />;
157                 default:
158                     return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
159             }
160         }
161
162         handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
163             event.preventDefault();
164             clearTimeout(this.timeout);
165             this.props.saveQuery(this.state.value);
166             this.props.onSearch(this.state.value);
167             this.props.loadQueries();
168         }
169
170         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
171             clearTimeout(this.timeout);
172             this.setState({ value: event.target.value });
173             this.timeout = window.setTimeout(
174                 () => this.props.onSearch(this.state.value),
175                 this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
176             );
177             if (event.target.value.length > 0) {
178                 this.props.onSetView(SearchView.AUTOCOMPLETE);
179             } else {
180                 this.props.onSetView(SearchView.BASIC);
181             }
182         }
183     }
184 );