--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export class SearchQueriesService {
+ private recentQueries: string[] = this.getRecentQueries();
+
+ saveRecentQuery(query: string) {
+ if (this.recentQueries.length >= 5) {
+ this.recentQueries.shift();
+ this.recentQueries.push(query);
+ } else {
+ this.recentQueries.push(query);
+ }
+ localStorage.setItem('recentQueries', JSON.stringify(this.recentQueries));
+ }
+
+ getRecentQueries() {
+ return JSON.parse(localStorage.getItem('recentQueries') || '[]') as string[];
+ }
+}
\ No newline at end of file
import { LogService } from './log-service/log-service';
import { ApiActions } from "~/services/api/api-actions";
import { WorkflowService } from "~/services/workflow-service/workflow-service";
+import { SearchQueriesService } from '~/services/search-service/search-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const collectionFilesService = new CollectionFilesService(collectionService);
const favoriteService = new FavoriteService(linkService, groupsService);
const tagService = new TagService(linkService);
+ const searchQueriesService = new SearchQueriesService();
return {
ancestorsService,
linkService,
logService,
projectService,
+ searchQueriesService,
tagService,
userService,
webdavClient,
export const goToView = (currentView: string) => searchBarActions.SET_CURRENT_VIEW(currentView);
-export const searchData = (searchValue: string) =>
+export const saveRecentQuery = (query: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ services.searchQueriesService.saveRecentQuery(query);
+ };
+
+export const loadRecentQueries = () =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const recentSearchQueries = services.searchQueriesService.getRecentQueries();
+ return recentSearchQueries || [];
+ };
+
+export const searchData = (searchValue: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
if (searchValue) {
const filters = getFilters('name', searchValue);
const { items } = await services.groupsService.contents('', {
- filters,
+ filters,
limit: 5,
recursive: true
});
}
};
-
const getFilters = (filterName: string, searchValue: string): string => {
return new FilterBuilder()
.addIsA("uuid", [ResourceKind.PROJECT, ResourceKind.COLLECTION, ResourceKind.PROCESS])
.addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT)
.addEqual('groupClass', GroupClass.PROJECT, GroupContentsResourcePrefix.PROJECT)
.getFilters();
-};
\ No newline at end of file
+};
import { SearchView } from '~/store/search-bar/search-bar-reducer';
import { RecentQueriesItem } from '~/views-components/search-bar/search-bar-view';
-type CssRules = 'list';
+type CssRules = 'list' | 'searchView';
const styles: StyleRulesCallback<CssRules> = theme => {
return {
list: {
padding: '0px'
+ },
+ searchView: {
+ borderRadius: `0 0 ${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px`
}
};
};
export const SearchBarAdvancedView = withStyles(styles)(
({ classes, setView }: SearchBarAdvancedViewProps & WithStyles<CssRules>) =>
- <Paper>
+ <Paper className={classes.searchView}>
<List component="nav" className={classes.list}>
<RecentQueriesItem text='ADVANCED VIEW' />
</List>
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Paper, StyleRulesCallback, withStyles, WithStyles, List, ListItem, ListItemText } from '@material-ui/core';
-import { ArvadosTheme } from '~/common/custom-theme';
+import { Paper, StyleRulesCallback, withStyles, WithStyles, List } from '@material-ui/core';
import { RecentQueriesItem } from '~/views-components/search-bar/search-bar-view';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
import Highlighter from "react-highlight-words";
-type CssRules = 'list';
+type CssRules = 'list' | 'searchView';
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- list: {
- padding: 0
- }
-});
+const styles: StyleRulesCallback<CssRules> = theme => {
+ return {
+ list: {
+ padding: 0
+ },
+ searchView: {
+ borderRadius: `0 0 ${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px`
+ }
+ };
+};
export interface SearchBarAutocompleteViewDataProps {
searchResults?: GroupContentsResource[];
type SearchBarAutocompleteViewProps = SearchBarAutocompleteViewDataProps & WithStyles<CssRules>;
export const SearchBarAutocompleteView = withStyles(styles)(
- ({ classes, searchResults, searchValue }: SearchBarAutocompleteViewProps ) =>
- <Paper>
- {searchResults && <List component="nav" className={classes.list}>
+ ({ classes, searchResults, searchValue }: SearchBarAutocompleteViewProps) =>
+ <Paper className={classes.searchView}>
+ {searchResults && <List component="nav" className={classes.list}>
{searchResults.map((item: GroupContentsResource) => {
return <RecentQueriesItem key={item.uuid} text={getFormattedText(item.name, searchValue)} />;
})}
padding: '0px'
},
searchView: {
- color: theme.palette.common.black
+ color: theme.palette.common.black,
+ borderRadius: `0 0 ${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px`
}
};
};
interface SearchBarBasicViewProps {
setView: (currentView: string) => void;
+ recentQueries: () => string[];
}
export const SearchBarBasicView = withStyles(styles)(
- ({ classes, setView }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
+ ({ classes, setView, recentQueries }: SearchBarBasicViewProps & WithStyles<CssRules>) =>
<Paper className={classes.searchView}>
<div className={classes.searchQueryList}>Saved search queries</div>
<List component="nav" className={classes.list}>
</List>
<div className={classes.searchQueryList}>Recent search queries</div>
<List component="nav" className={classes.list}>
- <RecentQueriesItem text="cos" />
- <RecentQueriesItem text="testtest" />
+ {recentQueries().map((query, index) => <RecentQueriesItem key={query + index} text={query} />)}
</List>
<div className={classes.advanced} onClick={() => setView(SearchView.ADVANCED)}>Advanced search</div>
</Paper>
import * as Adapter from 'enzyme-adapter-react-16';
+
configure({ adapter: new Adapter() });
-describe("<SearchBar />", () => {
+describe("<SearchBarView />", () => {
jest.useFakeTimers();
});
describe("on submit", () => {
- it("calls onSearch with initial value passed via props", () => {
- const searchBar = mount(<SearchBarView value="initial value" onSearch={onSearch} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
- searchBar.find("form").simulate("submit");
- expect(onSearch).toBeCalledWith("initial value");
- });
-
it("calls onSearch with current value", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="current value" {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
searchBar.find("form").simulate("submit");
expect(onSearch).toBeCalledWith("current value");
});
it("calls onSearch with new value passed via props", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
- searchBar.find("input").simulate("change", { target: { value: "current value" } });
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="current value" {...mockSearchProps()} />);
+ searchBar.find("input").simulate("change", { target: { value: "new value" } });
searchBar.setProps({ value: "new value" });
searchBar.find("form").simulate("submit");
expect(onSearch).toBeCalledWith("new value");
});
it("cancels timeout set on input value change", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} debounce={1000} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} debounce={1000} value="current value" {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
searchBar.find("form").simulate("submit");
jest.runTimersToTime(1000);
describe("on input value change", () => {
it("calls onSearch after default timeout", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="current value" {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
expect(onSearch).not.toBeCalled();
jest.runTimersToTime(DEFAULT_SEARCH_DEBOUNCE);
});
it("calls onSearch after the time specified in props has passed", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} debounce={2000} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="current value" debounce={2000} {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(1000);
expect(onSearch).not.toBeCalled();
});
it("calls onSearch only once after no change happened during the specified time", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} debounce={1000} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="current value" debounce={1000} {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(500);
searchBar.find("input").simulate("change", { target: { value: "changed value" } });
});
it("calls onSearch again after the specified time has passed since previous call", () => {
- const searchBar = mount(<SearchBarView value="" onSearch={onSearch} debounce={1000} currentView='' open={true} onSetView={jest.fn()} openView={jest.fn()} closeView={jest.fn()} />);
+ const searchBar = mount(<SearchBarView onSearch={onSearch} value="latest value" debounce={1000} {...mockSearchProps()} />);
searchBar.find("input").simulate("change", { target: { value: "current value" } });
jest.runTimersToTime(500);
searchBar.find("input").simulate("change", { target: { value: "intermediate value" } });
});
});
});
+
+const mockSearchProps = () => ({
+ currentView: '',
+ open: true,
+ onSetView: jest.fn(),
+ openView: jest.fn(),
+ loseView: jest.fn(),
+ closeView: jest.fn(),
+ saveQuery: jest.fn(),
+ loadQueries: () => ['test']
+});
\ No newline at end of file
import { SearchBarAutocompleteView, SearchBarAutocompleteViewDataProps } from '~/views-components/search-bar/search-bar-autocomplete-view';
import { ArvadosTheme } from '~/common/custom-theme';
-type CssRules = 'container' | 'input' | 'searchBar';
+type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'searchBar';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
return {
container: {
position: 'relative',
width: '100%',
- borderRadius: '0px'
+ borderRadius: theme.spacing.unit / 4
+ },
+ containerSearchViewOpened: {
+ position: 'relative',
+ width: '100%',
+ borderRadius: `${theme.spacing.unit / 4}px ${theme.spacing.unit / 4}px 0 0`
},
input: {
border: 'none',
onSetView: (currentView: string) => void;
openView: () => void;
closeView: () => void;
+ saveQuery: (query: string) => void;
+ loadQueries: () => string[];
}
type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
render() {
const { classes, currentView, openView, closeView, open } = this.props;
return <ClickAwayListener onClickAway={() => closeView()}>
- <Paper className={classes.container} >
+ <Paper className={open ? classes.containerSearchViewOpened : classes.container} >
<form onSubmit={this.handleSubmit} className={classes.searchBar}>
<Input
className={classes.input}
getView = (currentView: string) => {
switch (currentView) {
case SearchView.BASIC:
- return <SearchBarBasicView setView={this.props.onSetView} />;
+ return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
case SearchView.ADVANCED:
return <SearchBarAdvancedView setView={this.props.onSetView} />;
case SearchView.AUTOCOMPLETE:
searchResults={this.props.searchResults}
searchValue={this.props.searchValue} />;
default:
- return <SearchBarBasicView setView={this.props.onSetView} />;
+ return <SearchBarBasicView setView={this.props.onSetView} recentQueries={this.props.loadQueries} />;
}
}
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
clearTimeout(this.timeout);
+ this.props.saveQuery(this.state.value);
this.props.onSearch(this.state.value);
+ this.props.loadQueries();
}
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
import { Dispatch } from 'redux';
import { goToView, searchData, searchBarActions } from '~/store/search-bar/search-bar-actions';
import { SearchBarView } from '~/views-components/search-bar/search-bar-view';
+import { saveRecentQuery, loadRecentQueries } from '~/store/search-bar/search-bar-actions';
const mapStateToProps = ({ searchBar }: RootState) => {
return {
onSearch: (valueSearch: string) => dispatch<any>(searchData(valueSearch)),
onSetView: (currentView: string) => dispatch(goToView(currentView)),
openView: () => dispatch<any>(searchBarActions.OPEN_SEARCH_VIEW()),
- closeView: () => dispatch<any>(searchBarActions.CLOSE_SEARCH_VIEW())
+ closeView: () => dispatch<any>(searchBarActions.CLOSE_SEARCH_VIEW()),
+ saveQuery: (query: string) => dispatch<any>(saveRecentQuery(query)),
+ loadQueries: () => dispatch<any>(loadRecentQueries())
});
export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);
\ No newline at end of file
import { MuiThemeProvider } from '@material-ui/core/styles';
import { CustomTheme } from '~/common/custom-theme';
import { createServices } from "~/services/services";
+import 'jest-localstorage-mock';
const history = createBrowserHistory();