Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 13827-select-field...
[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 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: string[];
57 } & SearchBarAutocompleteViewDataProps;
58
59 interface SearchBarActionProps {
60     onSearch: (value: string) => any;
61     debounce?: number;
62     onSetView: (currentView: string) => void;
63     closeView: () => void;
64     saveRecentQuery: (query: string) => void;
65     loadRecentQueries: () => string[];
66     saveQuery: (data: SearchBarAdvanceFormData) => void;
67     deleteSavedQuery: (id: number) => void;
68     openSearchView: () => void;
69 }
70
71 type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
72
73 interface SearchBarState {
74     value: string;
75 }
76
77 interface RenderSavedQueriesProps {
78     text: string | JSX.Element;
79     id: number;
80     deleteSavedQuery: (id: number) => void;
81 }
82
83 interface RenderRecentQueriesProps {
84     text: string | JSX.Element;
85 }
86
87 export const RecentQueriesItem = (props: RenderRecentQueriesProps) => {
88     return <ListItem button>
89         <ListItemText secondary={props.text} />
90     </ListItem>;
91 };
92
93
94 export const RenderSavedQueries = (props: RenderSavedQueriesProps) => {
95     return <ListItem button>
96         <ListItemText secondary={props.text} />
97         <ListItemSecondaryAction>
98             <Tooltip title="Remove">
99                 <IconButton aria-label="Remove" onClick={() => props.deleteSavedQuery(props.id)}>
100                     <RemoveIcon />
101                 </IconButton>
102             </Tooltip>
103         </ListItemSecondaryAction>
104     </ListItem>;
105 };
106
107 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
108
109 export const SearchBarView = withStyles(styles)(
110     class extends React.Component<SearchBarProps> {
111         state: SearchBarState = {
112             value: ""
113         };
114
115         timeout: number;
116
117         render() {
118             const { classes, currentView, openSearchView, closeView, isPopoverOpen } = this.props;
119             return <ClickAwayListener onClickAway={() => closeView()}>
120                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
121                     <form onSubmit={this.handleSubmit}>
122                         <Input
123                             className={classes.input}
124                             onChange={this.handleChange}
125                             placeholder="Search"
126                             value={this.state.value}
127                             fullWidth={true}
128                             disableUnderline={true}
129                             onClick={openSearchView}
130                             endAdornment={
131                                 <InputAdornment position="end">
132                                     <Tooltip title='Search'>
133                                         <IconButton>
134                                             <SearchIcon />
135                                         </IconButton>
136                                     </Tooltip>
137                                 </InputAdornment>
138                             } />
139                     </form>
140                     <div className={classes.view}>
141                         {isPopoverOpen && this.getView(currentView)}
142                     </div>
143                 </Paper >
144             </ClickAwayListener>;
145         }
146
147         componentDidMount() {
148             this.setState({ value: this.props.searchValue });
149         }
150
151         componentWillReceiveProps(nextProps: SearchBarProps) {
152             if (nextProps.searchValue !== this.props.searchValue) {
153                 this.setState({ value: nextProps.searchValue });
154             }
155         }
156
157         componentWillUnmount() {
158             clearTimeout(this.timeout);
159         }
160
161         getView = (currentView: string) => {
162             const { onSetView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue, searchResults, saveQuery } = this.props;
163             switch (currentView) {
164                 case SearchView.BASIC:
165                     return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
166                 case SearchView.ADVANCED:
167                     return <SearchBarAdvancedView setView={onSetView} saveQuery={saveQuery}/>;
168                 case SearchView.AUTOCOMPLETE:
169                     return <SearchBarAutocompleteView
170                         searchResults={searchResults}
171                         searchValue={searchValue} />;
172                 default:
173                     return <SearchBarBasicView setView={onSetView} recentQueries={loadRecentQueries} savedQueries={savedQueries} deleteSavedQuery={deleteSavedQuery} />;
174             }
175         }
176
177         handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
178             event.preventDefault();
179             clearTimeout(this.timeout);
180             this.props.saveRecentQuery(this.state.value);
181             this.props.onSearch(this.state.value);
182             this.props.loadRecentQueries();
183         }
184
185         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
186             clearTimeout(this.timeout);
187             this.setState({ value: event.target.value });
188             this.timeout = window.setTimeout(
189                 () => this.props.onSearch(this.state.value),
190                 this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
191             );
192             if (event.target.value.length > 0) {
193                 this.props.onSetView(SearchView.AUTOCOMPLETE);
194             } else {
195                 this.props.onSetView(SearchView.BASIC);
196             }
197         }
198     }
199 );