Merge branch 'master' into 14313-basic-view-recent-queries
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 10 Oct 2018 07:51:02 +0000 (09:51 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 10 Oct 2018 07:51:02 +0000 (09:51 +0200)
refs #14313

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

src/services/search-service/search-service.ts [new file with mode: 0644]
src/services/services.ts
src/store/search-bar/search-bar-actions.ts
src/views-components/search-bar/search-bar-advanced-view.tsx
src/views-components/search-bar/search-bar-autocomplete-view.tsx
src/views-components/search-bar/search-bar-basic-view.tsx
src/views-components/search-bar/search-bar-view.test.tsx
src/views-components/search-bar/search-bar-view.tsx
src/views-components/search-bar/search-bar.tsx
src/views/workbench/workbench.test.tsx

diff --git a/src/services/search-service/search-service.ts b/src/services/search-service/search-service.ts
new file mode 100644 (file)
index 0000000..1fc61dd
--- /dev/null
@@ -0,0 +1,21 @@
+// 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
index d39a68b91c20df7d4ce896ae62f826d74bd53249..205d16c806e7abf3edd184b7de30c4ba1d66f68b 100644 (file)
@@ -22,6 +22,7 @@ import { ContainerService } from './container-service/container-service';
 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>;
 
@@ -48,6 +49,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
     const collectionFilesService = new CollectionFilesService(collectionService);
     const favoriteService = new FavoriteService(linkService, groupsService);
     const tagService = new TagService(linkService);
+    const searchQueriesService = new SearchQueriesService();
 
     return {
         ancestorsService,
@@ -63,6 +65,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
         linkService,
         logService,
         projectService,
+        searchQueriesService,
         tagService,
         userService,
         webdavClient,
index 2d171e02d59cb33d0c390a362d0298e123bd1f38..2b8ca83e694c62ff18cf457214f6852b363054c8 100644 (file)
@@ -23,14 +23,25 @@ export type SearchBarActions = UnionOf<typeof searchBarActions>;
 
 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
             });
@@ -38,7 +49,6 @@ export const searchData = (searchValue: string) =>
         }
     };
 
-
 const getFilters = (filterName: string, searchValue: string): string => {
     return new FilterBuilder()
         .addIsA("uuid", [ResourceKind.PROJECT, ResourceKind.COLLECTION, ResourceKind.PROCESS])
@@ -47,4 +57,4 @@ const getFilters = (filterName: string, searchValue: string): string => {
         .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT)
         .addEqual('groupClass', GroupClass.PROJECT, GroupContentsResourcePrefix.PROJECT)
         .getFilters();
-};
\ No newline at end of file
+};
index 356eb33fcbaf1898b737411198a9266bd7ebd330..dde23685f57b51f9b6802d661360507780c7f926 100644 (file)
@@ -7,12 +7,15 @@ import { Paper, StyleRulesCallback, withStyles, WithStyles, List, Button } from
 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`
         }
     };
 };
@@ -23,7 +26,7 @@ interface SearchBarAdvancedViewProps {
 
 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>
index c4b2457d45bc9db37b7f0cfdc7e9762e8ce9958c..affaf5310cc561e0e60d9b5497dd0d47bce60067 100644 (file)
@@ -3,19 +3,23 @@
 // 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[];
@@ -25,9 +29,9 @@ export interface SearchBarAutocompleteViewDataProps {
 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)} />;
                 })}
index c2bca73e13a1df9faba331d0c2b2456c8a6aa3e2..7f90ecdee5ccb0f98a100ef23cdaf92324529a6d 100644 (file)
@@ -28,17 +28,19 @@ const styles: StyleRulesCallback<CssRules> = theme => {
             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}>
@@ -47,8 +49,7 @@ export const SearchBarBasicView = withStyles(styles)(
             </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>
index 4230aa80a68caba45df435075a3268894cd255bc..5cd1545c2f04f8f826f25965770265a9bb2d09bc 100644 (file)
@@ -8,9 +8,10 @@ import { SearchBarView, DEFAULT_SEARCH_DEBOUNCE } from "./search-bar-view";
 
 import * as Adapter from 'enzyme-adapter-react-16';
 
+
 configure({ adapter: new Adapter() });
 
-describe("<SearchBar />", () => {
+describe("<SearchBarView />", () => {
 
     jest.useFakeTimers();
 
@@ -21,29 +22,23 @@ describe("<SearchBar />", () => {
     });
 
     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);
@@ -55,7 +50,7 @@ describe("<SearchBar />", () => {
 
     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);
@@ -63,7 +58,7 @@ describe("<SearchBar />", () => {
         });
 
         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();
@@ -72,7 +67,7 @@ describe("<SearchBar />", () => {
         });
 
         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" } });
@@ -81,7 +76,7 @@ describe("<SearchBar />", () => {
         });
 
         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" } });
@@ -95,3 +90,14 @@ describe("<SearchBar />", () => {
         });
     });
 });
+
+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
index f26cb7e6909139f613923e3858b97ff9c9c4fbff..b2575a8f5f3ec171c24f192ab235262aa9295334 100644 (file)
@@ -22,14 +22,19 @@ import { SearchBarAdvancedView } from '~/views-components/search-bar/search-bar-
 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',
@@ -53,6 +58,8 @@ interface SearchBarActionProps {
     onSetView: (currentView: string) => void;
     openView: () => void;
     closeView: () => void;
+    saveQuery: (query: string) => void;
+    loadQueries: () => string[];
 }
 
 type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
@@ -98,7 +105,7 @@ export const SearchBarView = withStyles(styles)(
         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}
@@ -140,7 +147,7 @@ export const SearchBarView = withStyles(styles)(
         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:
@@ -148,14 +155,16 @@ export const SearchBarView = withStyles(styles)(
                                 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>) => {
index df8808c115a3fbe5ec2fe484289aadeeb41d465c..98440fda29517e7a62d2337fd04fe9b1572a2b74 100644 (file)
@@ -7,6 +7,7 @@ import { RootState } from '~/store/store';
 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 {
@@ -21,7 +22,9 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
     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
index 14ca6947edfe967c6b3892f2b4340adf156f37a8..29eed6023762007273abec55a6b930ec156913d4 100644 (file)
@@ -12,6 +12,7 @@ import { ConnectedRouter } from "react-router-redux";
 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();