1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from "react";
6 import { compose } from "redux";
7 import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip, InputAdornment, Input } from "@material-ui/core";
8 import SearchIcon from "@material-ui/icons/Search";
9 import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
10 import { ArvadosTheme } from "common/custom-theme";
11 import { SearchView } from "store/search-bar/search-bar-reducer";
12 import { SearchBarBasicView, SearchBarBasicViewDataProps, SearchBarBasicViewActionProps } from "views-components/search-bar/search-bar-basic-view";
14 SearchBarAutocompleteView,
15 SearchBarAutocompleteViewDataProps,
16 SearchBarAutocompleteViewActionProps,
17 } from "views-components/search-bar/search-bar-autocomplete-view";
19 SearchBarAdvancedView,
20 SearchBarAdvancedViewDataProps,
21 SearchBarAdvancedViewActionProps,
22 } from "views-components/search-bar/search-bar-advanced-view";
23 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "common/codes";
24 import { debounce } from "debounce";
25 import { Vocabulary } from "models/vocabulary";
26 import { connectVocabulary } from "../resource-properties-form/property-field-common";
28 type CssRules = "container" | "containerSearchViewOpened" | "input" | "view";
30 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
35 borderRadius: theme.spacing.unit / 2,
36 zIndex: theme.zIndex.modal,
38 containerSearchViewOpened: {
41 borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
42 zIndex: theme.zIndex.modal,
56 export type SearchBarDataProps = SearchBarViewDataProps &
57 SearchBarAutocompleteViewDataProps &
58 SearchBarAdvancedViewDataProps &
59 SearchBarBasicViewDataProps;
61 interface SearchBarViewDataProps {
64 isPopoverOpen: boolean;
66 vocabulary?: Vocabulary;
69 export type SearchBarActionProps = SearchBarViewActionProps &
70 SearchBarAutocompleteViewActionProps &
71 SearchBarAdvancedViewActionProps &
72 SearchBarBasicViewActionProps;
74 interface SearchBarViewActionProps {
75 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
76 onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
77 onSetView: (currentView: string) => void;
78 closeView: () => void;
79 openSearchView: () => void;
80 loadRecentQueries: () => string[];
83 setAdvancedDataFromSearchValue: (search: string, vocabulary?: Vocabulary) => void;
86 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
88 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
89 if (e.keyCode === KEY_CODE_DOWN) {
91 if (!props.isPopoverOpen) {
92 props.onSetView(SearchView.AUTOCOMPLETE);
93 props.openSearchView();
97 } else if (e.keyCode === KEY_CODE_UP) {
100 } else if (e.keyCode === KEY_CODE_ESC) {
103 } else if (e.keyCode === KEY_ENTER) {
104 if (props.currentView === SearchView.BASIC) {
106 props.onSearch(props.selectedItem.query);
107 } else if (props.currentView === SearchView.AUTOCOMPLETE) {
108 if (props.selectedItem.id !== props.searchValue) {
110 props.navigateTo(props.selectedItem.id);
116 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
117 if (props.searchValue) {
118 props.onSetView(SearchView.AUTOCOMPLETE);
120 props.onSetView(SearchView.BASIC);
122 props.openSearchView();
125 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
127 if (props.isPopoverOpen && props.currentView === SearchView.ADVANCED) {
130 props.setAdvancedDataFromSearchValue(props.searchValue, props.vocabulary);
131 props.onSetView(SearchView.ADVANCED);
135 export const SearchBarView = compose(
139 class extends React.Component<SearchBarViewProps> {
140 debouncedSearch = debounce(() => {
141 this.props.onSearch(this.props.searchValue);
144 handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
145 this.debouncedSearch();
146 this.props.onChange(event);
149 handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
150 this.debouncedSearch.clear();
151 this.props.onSubmit(event);
154 componentWillUnmount() {
155 this.debouncedSearch.clear();
159 const { children, ...props } = this.props;
160 const { classes, isPopoverOpen } = this.props;
163 {isPopoverOpen && <Backdrop onClick={props.closeView} />}
165 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
167 data-cy="searchbar-parent-form"
168 onSubmit={this.handleSubmit}>
170 data-cy="searchbar-input-field"
171 className={classes.input}
172 onChange={this.handleChange}
174 value={props.searchValue}
176 disableUnderline={true}
177 onClick={e => handleInputClick(e, props)}
178 onKeyDown={e => handleKeyDown(e, props)}
180 <InputAdornment position="start">
181 <Tooltip title="Search">
182 <IconButton type="submit">
189 <InputAdornment position="end">
190 <Tooltip title="Advanced search">
191 <IconButton onClick={e => handleDropdownClick(e, props)}>
192 <ArrowDropDownIcon />
199 <div className={classes.view}>{isPopoverOpen && getView({ ...props })}</div>
207 const getView = (props: SearchBarViewProps) => {
208 switch (props.currentView) {
209 case SearchView.AUTOCOMPLETE:
211 <SearchBarAutocompleteView
212 navigateTo={props.navigateTo}
213 searchResults={props.searchResults}
214 searchValue={props.searchValue}
215 selectedItem={props.selectedItem}
218 case SearchView.ADVANCED:
220 <SearchBarAdvancedView
221 closeAdvanceView={props.closeAdvanceView}
223 saveQuery={props.saveQuery}
229 onSetView={props.onSetView}
230 onSearch={props.onSearch}
231 loadRecentQueries={props.loadRecentQueries}
232 savedQueries={props.savedQueries}
233 deleteSavedQuery={props.deleteSavedQuery}
234 editSavedQuery={props.editSavedQuery}
235 selectedItem={props.selectedItem}
241 const Backdrop = withStyles<"backdrop">(theme => ({
248 zIndex: theme.zIndex.modal,
250 }))(({ classes, ...props }: WithStyles<"backdrop"> & React.HTMLProps<HTMLDivElement>) => (
252 className={classes.backdrop}