refs #14364 Merge branch 'origin/14364-searchbar-arrow-navigation'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 30 Oct 2018 13:40:43 +0000 (14:40 +0100)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 30 Oct 2018 13:41:00 +0000 (14:41 +0100)
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

src/common/codes.ts [new file with mode: 0644]
src/services/search-service/search-service.ts
src/store/search-bar/search-bar-actions.ts
src/store/search-bar/search-bar-reducer.ts
src/store/search-results-panel/search-results-middleware-service.ts
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-recent-queries.tsx
src/views-components/search-bar/search-bar-save-queries.tsx
src/views-components/search-bar/search-bar-view.tsx
src/views-components/search-bar/search-bar.tsx

diff --git a/src/common/codes.ts b/src/common/codes.ts
new file mode 100644 (file)
index 0000000..6342a29
--- /dev/null
@@ -0,0 +1,8 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const KEY_CODE_UP = 38;
+export const KEY_CODE_DOWN = 40;
+export const KEY_CODE_ESC = 27;
+export const KEY_ENTER = 13;
index 5817275e0a34757d99acffe37bb5bfad1109557a..a8e91c39633b6007ad89cb2aa354a9fbe58fc1f4 100644 (file)
@@ -5,21 +5,19 @@
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
 
 export class SearchService {
-    private recentQueries: string[] = this.getRecentQueries();
+    private recentQueries = this.getRecentQueries();
     private savedQueries: SearchBarAdvanceFormData[] = this.getSavedQueries();
 
     saveRecentQuery(query: string) {
         if (this.recentQueries.length >= MAX_NUMBER_OF_RECENT_QUERIES) {
             this.recentQueries.shift();
-            this.recentQueries.push(query);
-        } else {
-            this.recentQueries.push(query);
         }
+        this.recentQueries.push(query);
         localStorage.setItem('recentQueries', JSON.stringify(this.recentQueries));
     }
 
-    getRecentQueries() {
-        return JSON.parse(localStorage.getItem('recentQueries') || '[]') as string[];
+    getRecentQueries(): string[] {
+        return JSON.parse(localStorage.getItem('recentQueries') || '[]');
     }
 
     saveQuery(data: SearchBarAdvanceFormData) {
@@ -43,4 +41,4 @@ export class SearchService {
     }
 }
 
-const MAX_NUMBER_OF_RECENT_QUERIES = 5;
\ No newline at end of file
+const MAX_NUMBER_OF_RECENT_QUERIES = 5;
index aea75303bf06b8bbd1ea280717e991488032fe23..165392c6c20ef0dc9fef9bdbfe559608529a623a 100644 (file)
@@ -26,7 +26,12 @@ export const searchBarActions = unionize({
     SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
     SET_SEARCH_VALUE: ofType<string>(),
     SET_SAVED_QUERIES: ofType<SearchBarAdvanceFormData[]>(),
-    UPDATE_SAVED_QUERY: ofType<SearchBarAdvanceFormData[]>()
+    SET_RECENT_QUERIES: ofType<string[]>(),
+    UPDATE_SAVED_QUERY: ofType<SearchBarAdvanceFormData[]>(),
+    SET_SELECTED_ITEM: ofType<string>(),
+    MOVE_UP: ofType<{}>(),
+    MOVE_DOWN: ofType<{}>(),
+    SELECT_FIRST_ITEM: ofType<{}>()
 });
 
 export type SearchBarActions = UnionOf<typeof searchBarActions>;
@@ -46,47 +51,45 @@ export const saveRecentQuery = (query: string) =>
 
 export const loadRecentQueries = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const recentSearchQueries = services.searchService.getRecentQueries();
-        return recentSearchQueries || [];
+        const recentQueries = services.searchService.getRecentQueries();
+        dispatch(searchBarActions.SET_RECENT_QUERIES(recentQueries));
+        return recentQueries;
     };
 
 export const searchData = (searchValue: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch, getState: () => RootState) => {
         const currentView = getState().searchBar.currentView;
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
-        dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
-        dispatch<any>(searchGroups(searchValue, 5, {}));
-        if (currentView === SearchView.BASIC) {
-            dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
-            dispatch(navigateToSearchResults);
+        if (searchValue.length > 0) {
+            dispatch<any>(searchGroups(searchValue, 5, {}));
+            if (currentView === SearchView.BASIC) {
+                dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
+                dispatch(navigateToSearchResults);
+            }
         }
-
     };
 
 export const searchAdvanceData = (data: SearchBarAdvanceFormData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const searchValue = getState().searchBar.searchValue;
+    async (dispatch: Dispatch) => {
         dispatch<any>(saveQuery(data));
-        dispatch<any>(searchGroups(searchValue, 100, data));
         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
         dispatch(navigateToSearchResults);
     };
 
-// Todo: create ids for particular searchQuery
 const saveQuery = (data: SearchBarAdvanceFormData) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const savedSearchQueries = services.searchService.getSavedQueries();
-        const filteredQuery = savedSearchQueries.find(query => query.searchQuery === data.searchQuery);
+        const savedQueries = services.searchService.getSavedQueries();
         if (data.saveQuery && data.searchQuery) {
+            const filteredQuery = savedQueries.find(query => query.searchQuery === data.searchQuery);
             if (filteredQuery) {
                 services.searchService.editSavedQueries(data);
-                dispatch(searchBarActions.UPDATE_SAVED_QUERY(savedSearchQueries));
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been sucessfully updated', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+                dispatch(searchBarActions.UPDATE_SAVED_QUERY(savedQueries));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully updated', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
             } else {
                 services.searchService.saveQuery(data);
-                dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been sucessfully saved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+                dispatch(searchBarActions.SET_SAVED_QUERIES(savedQueries));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully saved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
             }
         }
     };
@@ -100,7 +103,7 @@ export const deleteSavedQuery = (id: number) =>
     };
 
 export const editSavedQuery = (data: SearchBarAdvanceFormData) =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch<any>) => {
         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED));
         dispatch(searchBarActions.SET_SEARCH_VALUE(data.searchQuery));
         dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
@@ -108,34 +111,34 @@ export const editSavedQuery = (data: SearchBarAdvanceFormData) =>
 
 export const openSearchView = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(searchBarActions.OPEN_SEARCH_VIEW());
         const savedSearchQueries = services.searchService.getSavedQueries();
         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
+        dispatch(loadRecentQueries());
+        dispatch(searchBarActions.OPEN_SEARCH_VIEW());
+        dispatch(searchBarActions.SELECT_FIRST_ITEM());
     };
 
 export const closeSearchView = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const isOpen = getState().searchBar.open;
-        if (isOpen) {
-            dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
-            dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
-        }
+    (dispatch: Dispatch<any>) => {
+        dispatch(searchBarActions.SET_SELECTED_ITEM(''));
+        dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
     };
 
 export const closeAdvanceView = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch<any>) => {
         dispatch(searchBarActions.SET_SEARCH_VALUE(''));
         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
     };
 
 export const navigateToItem = (uuid: string) =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch<any>) => {
+        dispatch(searchBarActions.SET_SELECTED_ITEM(''));
         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
         dispatch(navigateTo(uuid));
     };
 
 export const changeData = (searchValue: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch, getState: () => RootState) => {
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
         const currentView = getState().searchBar.currentView;
         const searchValuePresent = searchValue.length > 0;
@@ -143,37 +146,32 @@ export const changeData = (searchValue: string) =>
         if (currentView === SearchView.ADVANCED) {
 
         } else if (searchValuePresent) {
-            dispatch<any>(goToView(SearchView.AUTOCOMPLETE));
+            dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.AUTOCOMPLETE));
+            dispatch(searchBarActions.SET_SELECTED_ITEM(searchValue));
             debounceStartSearch(dispatch);
         } else {
-            dispatch<any>(goToView(SearchView.BASIC));
-            dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
+            dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
             dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+            dispatch(searchBarActions.SELECT_FIRST_ITEM());
         }
     };
 
 export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch, getState: () => RootState) => {
         event.preventDefault();
         const searchValue = getState().searchBar.searchValue;
         dispatch<any>(saveRecentQuery(searchValue));
-        dispatch<any>(searchDataOnEnter(searchValue));
         dispatch<any>(loadRecentQueries());
-    };
-
-const searchDataOnEnter = (searchValue: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
         dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
-        dispatch<any>(searchGroups(searchValue, 100, {}));
         dispatch(navigateToSearchResults);
     };
 
 const debounceStartSearch = debounce((dispatch: Dispatch) => dispatch<any>(startSearch()), DEFAULT_SEARCH_DEBOUNCE);
 
 const startSearch = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch, getState: () => RootState) => {
         const searchValue = getState().searchBar.searchValue;
         dispatch<any>(searchData(searchValue));
     };
@@ -215,16 +213,26 @@ const buildDateFilter = (date?: string): string => {
 };
 
 export const initAdvanceFormProjectsTree = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch) => {
         dispatch<any>(initUserProject(SEARCH_BAR_ADVANCE_FORM_PICKER_ID));
     };
 
 export const changeAdvanceFormProperty = (property: string, value: PropertyValues[] | string = '') =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch) => {
         dispatch(change(SEARCH_BAR_ADVANCE_FORM_NAME, property, value));
     };
 
 export const updateAdvanceFormProperties = (propertyValues: PropertyValues) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch) => {
         dispatch(arrayPush(SEARCH_BAR_ADVANCE_FORM_NAME, 'properties', propertyValues));
-    };
\ No newline at end of file
+    };
+
+export const moveUp = () =>
+    (dispatch: Dispatch) => {
+        dispatch(searchBarActions.MOVE_UP());
+    };
+
+export const moveDown = () =>
+    (dispatch: Dispatch) => {
+        dispatch(searchBarActions.MOVE_DOWN());
+    };
index 781246a3c5147917394b3cd997d2aef864a49d68..8508c05d044cee1668ad04272f69fb427342b828 100644 (file)
@@ -6,12 +6,20 @@ import { searchBarActions, SearchBarActions } from '~/store/search-bar/search-ba
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
 
+type SearchResult = GroupContentsResource;
+export type SearchBarSelectedItem = {
+    id: string,
+    query: string
+};
+
 interface SearchBar {
     currentView: string;
     open: boolean;
-    searchResults: GroupContentsResource[];
+    searchResults: SearchResult[];
     searchValue: string;
     savedQueries: SearchBarAdvanceFormData[];
+    recentQueries: string[];
+    selectedItem: SearchBarSelectedItem;
 }
 
 export enum SearchView {
@@ -25,17 +33,111 @@ const initialState: SearchBar = {
     open: false,
     searchResults: [],
     searchValue: '',
-    savedQueries: []
+    savedQueries: [],
+    recentQueries: [],
+    selectedItem: {
+        id: '',
+        query: ''
+    }
+};
+
+const makeSelectedItem = (id: string, query?: string): SearchBarSelectedItem => ({ id, query: query ? query : id });
+
+const makeQueryList = (recentQueries: string[], savedQueries: SearchBarAdvanceFormData[]) => {
+    const recentIds = recentQueries.map((q, idx) => makeSelectedItem(`RQ-${idx}-${q}`, q));
+    const savedIds = savedQueries.map((q, idx) => makeSelectedItem(`SQ-${idx}-${q.searchQuery}`, q.searchQuery));
+    return recentIds.concat(savedIds);
 };
 
 export const searchBarReducer = (state = initialState, action: SearchBarActions): SearchBar =>
     searchBarActions.match(action, {
-        SET_CURRENT_VIEW: currentView => ({ ...state, currentView }),
+        SET_CURRENT_VIEW: currentView => ({
+            ...state,
+            currentView,
+            open: true
+        }),
         OPEN_SEARCH_VIEW: () => ({ ...state, open: true }),
         CLOSE_SEARCH_VIEW: () => ({ ...state, open: false }),
-        SET_SEARCH_RESULTS: (searchResults) => ({ ...state, searchResults }),
-        SET_SEARCH_VALUE: (searchValue) => ({ ...state, searchValue }),
+        SET_SEARCH_RESULTS: searchResults => ({
+            ...state,
+            searchResults,
+            selectedItem: makeSelectedItem(searchResults.length > 0
+                ? searchResults.findIndex(r => r.uuid === state.selectedItem.id) >= 0
+                    ? state.selectedItem.id
+                    : state.searchValue
+                : state.searchValue
+            )
+        }),
+        SET_SEARCH_VALUE: searchValue => ({
+            ...state,
+            searchValue
+        }),
         SET_SAVED_QUERIES: savedQueries => ({ ...state, savedQueries }),
+        SET_RECENT_QUERIES: recentQueries => ({ ...state, recentQueries }),
         UPDATE_SAVED_QUERY: searchQuery => ({ ...state, savedQueries: searchQuery }),
+        SET_SELECTED_ITEM: item => ({ ...state, selectedItem: makeSelectedItem(item) }),
+        MOVE_UP: () => {
+            let selectedItem = state.selectedItem;
+            if (state.currentView === SearchView.AUTOCOMPLETE) {
+                const idx = state.searchResults.findIndex(r => r.uuid === selectedItem.id);
+                if (idx > 0) {
+                    selectedItem = makeSelectedItem(state.searchResults[idx - 1].uuid);
+                } else {
+                    selectedItem = makeSelectedItem(state.searchValue);
+                }
+            } else if (state.currentView === SearchView.BASIC) {
+                const items = makeQueryList(state.recentQueries, state.savedQueries);
+
+                const idx = items.findIndex(i => i.id === selectedItem.id);
+                if (idx > 0) {
+                    selectedItem = items[idx - 1];
+                }
+            }
+            return {
+                ...state,
+                selectedItem
+            };
+        },
+        MOVE_DOWN: () => {
+            let selectedItem = state.selectedItem;
+            if (state.currentView === SearchView.AUTOCOMPLETE) {
+                const idx = state.searchResults.findIndex(r => r.uuid === selectedItem.id);
+                if (idx >= 0 && idx < state.searchResults.length - 1) {
+                    selectedItem = makeSelectedItem(state.searchResults[idx + 1].uuid);
+                } else if (idx < 0 && state.searchResults.length > 0) {
+                    selectedItem = makeSelectedItem(state.searchResults[0].uuid);
+                }
+            } else if (state.currentView === SearchView.BASIC) {
+                const items = makeQueryList(state.recentQueries, state.savedQueries);
+
+                const idx = items.findIndex(i => i.id === selectedItem.id);
+                if (idx >= 0 && idx < items.length - 1) {
+                    selectedItem = items[idx + 1];
+                }
+
+                if (idx < 0 && items.length > 0) {
+                    selectedItem = items[0];
+                }
+            }
+            return {
+                ...state,
+                selectedItem
+            };
+        },
+        SELECT_FIRST_ITEM: () => {
+            let selectedItem = state.selectedItem;
+            if (state.currentView === SearchView.AUTOCOMPLETE) {
+                selectedItem = makeSelectedItem(state.searchValue);
+            } else if (state.currentView === SearchView.BASIC) {
+                const items = makeQueryList(state.recentQueries, state.savedQueries);
+                if (items.length > 0) {
+                    selectedItem = items[0];
+                }
+            }
+            return {
+                ...state,
+                selectedItem
+            };
+        },
         default: () => state
-    });
\ No newline at end of file
+    });
index 0b38444205e496ed0d6d440e00fe16b214232fcc..e8097e9838b791dd8c36481b3b678dc0dca7292d 100644 (file)
@@ -28,7 +28,7 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         const searchValue = state.searchBar.searchValue;
         try {
-            const response = await this.services.groupsService.contents(userUuid, getParams(dataExplorer, searchValue));
+            const response = await this.services.groupsService.contents('', getParams(dataExplorer, searchValue));
             api.dispatch(updateResources(response.items));
             api.dispatch(setItems(response));
         } catch {
@@ -72,4 +72,4 @@ const couldNotFetchWorkflows = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch workflows.',
         kind: SnackbarKind.ERROR
-    });
\ No newline at end of file
+    });
index 69fa459e637dc1837482fd0226a110f27688f4af..4dab5db0890ab97acd5f46e69b29815504711903 100644 (file)
@@ -6,6 +6,7 @@ import * as React from 'react';
 import { Paper, StyleRulesCallback, withStyles, WithStyles, List, ListItem, ListItemText } from '@material-ui/core';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import Highlighter from "react-highlight-words";
+import { SearchBarSelectedItem } from "~/store/search-bar/search-bar-reducer";
 
 type CssRules = 'searchView' | 'list' | 'listItem';
 
@@ -20,14 +21,14 @@ const styles: StyleRulesCallback<CssRules> = theme => {
         listItem: {
             paddingLeft: theme.spacing.unit,
             paddingRight: theme.spacing.unit * 2,
-        },
-        
+        }
     };
 };
 
 export interface SearchBarAutocompleteViewDataProps {
-    searchResults?: GroupContentsResource[];
+    searchResults: GroupContentsResource[];
     searchValue?: string;
+    selectedItem: SearchBarSelectedItem;
 }
 
 export interface SearchBarAutocompleteViewActionProps {
@@ -37,18 +38,23 @@ export interface SearchBarAutocompleteViewActionProps {
 type SearchBarAutocompleteViewProps = SearchBarAutocompleteViewDataProps & SearchBarAutocompleteViewActionProps & WithStyles<CssRules>;
 
 export const SearchBarAutocompleteView = withStyles(styles)(
-    ({ classes, searchResults, searchValue, navigateTo }: SearchBarAutocompleteViewProps) =>
-        <Paper className={classes.searchView}>
-            {searchResults && <List component="nav" className={classes.list}>
+    ({ classes, searchResults, searchValue, navigateTo, selectedItem }: SearchBarAutocompleteViewProps) => {
+        console.log(searchValue, selectedItem);
+        return <Paper className={classes.searchView}>
+            <List component="nav" className={classes.list}>
+                <ListItem button className={classes.listItem} selected={!selectedItem || searchValue === selectedItem.id}>
+                    <ListItemText secondary={searchValue}/>
+                </ListItem>
                 {searchResults.map((item: GroupContentsResource) =>
-                    <ListItem button key={item.uuid} className={classes.listItem}>
-                        <ListItemText secondary={getFormattedText(item.name, searchValue)} onClick={() => navigateTo(item.uuid)} />
+                    <ListItem button key={item.uuid} className={classes.listItem} selected={item.uuid === selectedItem.id}>
+                        <ListItemText secondary={getFormattedText(item.name, searchValue)}
+                                      onClick={() => navigateTo(item.uuid)}/>
                     </ListItem>
                 )}
-            </List>}
-        </Paper>
-);
+            </List>
+        </Paper>;
+    });
 
 const getFormattedText = (textToHighlight: string, searchString = '') => {
     return <Highlighter searchWords={[searchString]} autoEscape={true} textToHighlight={textToHighlight} />;
-};
\ No newline at end of file
+};
index 22dfa829113b214582c259b8984f1d89143d2adc..76d46b3662573cd78a0541090aaa4b3ec00fbe7a 100644 (file)
@@ -7,7 +7,7 @@ import { Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/
 import { SearchView } from '~/store/search-bar/search-bar-reducer';
 import {
     SearchBarRecentQueries,
-    SearchBarRecentQueriesActionProps 
+    SearchBarRecentQueriesActionProps
 } from '~/views-components/search-bar/search-bar-recent-queries';
 import {
     SearchBarSavedQueries,
@@ -33,7 +33,7 @@ const styles: StyleRulesCallback<CssRules> = theme => {
         },
         label: {
             fontSize: '0.875rem',
-            padding: `${theme.spacing.unit / 2}px ${theme.spacing.unit}px `,
+            padding: `${theme.spacing.unit}px ${theme.spacing.unit}px `,
             color: theme.palette.grey["900"],
             background: theme.palette.grey["200"]
         }
@@ -50,18 +50,20 @@ export type SearchBarBasicViewActionProps = {
 type SearchBarBasicViewProps = SearchBarBasicViewDataProps & SearchBarBasicViewActionProps & WithStyles<CssRules>;
 
 export const SearchBarBasicView = withStyles(styles)(
-    ({ classes, onSetView, loadRecentQueries, deleteSavedQuery, savedQueries, onSearch, editSavedQuery }: SearchBarBasicViewProps) =>
+    ({ classes, onSetView, loadRecentQueries, deleteSavedQuery, savedQueries, onSearch, editSavedQuery, selectedItem }: SearchBarBasicViewProps) =>
         <Paper className={classes.root}>
             <div className={classes.label}>Recent search queries</div>
             <SearchBarRecentQueries
                 onSearch={onSearch}
-                loadRecentQueries={loadRecentQueries} />
+                loadRecentQueries={loadRecentQueries}
+                selectedItem={selectedItem} />
             <div className={classes.label}>Saved search queries</div>
             <SearchBarSavedQueries
                 onSearch={onSearch}
                 savedQueries={savedQueries}
                 editSavedQuery={editSavedQuery}
-                deleteSavedQuery={deleteSavedQuery} />
+                deleteSavedQuery={deleteSavedQuery}
+                selectedItem={selectedItem} />
             <div className={classes.advanced} onClick={() => onSetView(SearchView.ADVANCED)}>Advanced search</div>
         </Paper>
-);
\ No newline at end of file
+);
index 3de3ca7a1f54e9e45b6a2e40d04447711f84d23b..a3c03e4074b11b15dff0e5c9716765005bb85d6a 100644 (file)
@@ -5,6 +5,7 @@
 import * as React from 'react';
 import { withStyles, WithStyles, StyleRulesCallback, List, ListItem, ListItemText } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
+import { SearchBarSelectedItem } from "~/store/search-bar/search-bar-reducer";
 
 type CssRules = 'root' | 'listItem' | 'listItemText';
 
@@ -22,22 +23,26 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
+export interface SearchBarRecentQueriesDataProps {
+    selectedItem: SearchBarSelectedItem;
+}
+
 export interface SearchBarRecentQueriesActionProps {
     onSearch: (searchValue: string) => void;
     loadRecentQueries: () => string[];
 }
 
-type SearchBarRecentQueriesProps = SearchBarRecentQueriesActionProps & WithStyles<CssRules>;
+type SearchBarRecentQueriesProps = SearchBarRecentQueriesDataProps & SearchBarRecentQueriesActionProps & WithStyles<CssRules>;
 
 export const SearchBarRecentQueries = withStyles(styles)(
-    ({ classes, onSearch, loadRecentQueries }: SearchBarRecentQueriesProps) =>
+    ({ classes, onSearch, loadRecentQueries, selectedItem }: SearchBarRecentQueriesProps) =>
         <List component="nav" className={classes.root}>
             {loadRecentQueries().map((query, index) =>
-                <ListItem button key={index} className={classes.listItem}>
-                    <ListItemText disableTypography 
-                        secondary={query} 
-                        onClick={() => onSearch(query)} 
+                <ListItem button key={index} className={classes.listItem} selected={`RQ-${index}-${query}` === selectedItem.id}>
+                    <ListItemText disableTypography
+                        secondary={query}
+                        onClick={() => onSearch(query)}
                         className={classes.listItemText} />
                 </ListItem>
             )}
-        </List>);
\ No newline at end of file
+        </List>);
index ccf10a1b84fcc112be2dba0fd8e3f9ca3a350d0a..aa62c58f97214918cfc1bbf06be3c20ae93808bf 100644 (file)
@@ -7,6 +7,7 @@ import { withStyles, WithStyles, StyleRulesCallback, List, ListItem, ListItemTex
 import { ArvadosTheme } from '~/common/custom-theme';
 import { RemoveIcon, EditSavedQueryIcon } from '~/components/icon/icon';
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
+import { SearchBarSelectedItem } from "~/store/search-bar/search-bar-reducer";
 
 type CssRules = 'root' | 'listItem' | 'listItemText' | 'button';
 
@@ -30,6 +31,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 export interface SearchBarSavedQueriesDataProps {
     savedQueries: SearchBarAdvanceFormData[];
+    selectedItem: SearchBarSelectedItem;
 }
 
 export interface SearchBarSavedQueriesActionProps {
@@ -38,18 +40,18 @@ export interface SearchBarSavedQueriesActionProps {
     editSavedQuery: (data: SearchBarAdvanceFormData, id: number) => void;
 }
 
-type SearchBarSavedQueriesProps = SearchBarSavedQueriesDataProps 
-    & SearchBarSavedQueriesActionProps 
+type SearchBarSavedQueriesProps = SearchBarSavedQueriesDataProps
+    & SearchBarSavedQueriesActionProps
     & WithStyles<CssRules>;
 
 export const SearchBarSavedQueries = withStyles(styles)(
-    ({ classes, savedQueries, onSearch, editSavedQuery, deleteSavedQuery }: SearchBarSavedQueriesProps) =>
+    ({ classes, savedQueries, onSearch, editSavedQuery, deleteSavedQuery, selectedItem }: SearchBarSavedQueriesProps) =>
         <List component="nav" className={classes.root}>
-            {savedQueries.map((query, index) => 
-                <ListItem button key={index} className={classes.listItem}>
-                    <ListItemText disableTypography 
-                        secondary={query.searchQuery} 
-                        onClick={() => onSearch(query.searchQuery)} 
+            {savedQueries.map((query, index) =>
+                <ListItem button key={index} className={classes.listItem} selected={`SQ-${index}-${query.searchQuery}` === selectedItem.id}>
+                    <ListItemText disableTypography
+                        secondary={query.searchQuery}
+                        onClick={() => onSearch(query.searchQuery)}
                         className={classes.listItemText} />
                     <ListItemSecondaryAction>
                         <Tooltip title="Edit">
@@ -65,4 +67,4 @@ export const SearchBarSavedQueries = withStyles(styles)(
                     </ListItemSecondaryAction>
                 </ListItem>
             )}
-    </List>);
\ No newline at end of file
+    </List>);
index b3ec7ab71cce87aff5441acacd2de13c40b5e25b..1a19b47d67dccbac6cc643f7caeb13006c9ee3a2 100644 (file)
@@ -31,6 +31,7 @@ import {
     SearchBarAdvancedViewDataProps,
     SearchBarAdvancedViewActionProps
 } from '~/views-components/search-bar/search-bar-advanced-view';
+import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
 
 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
 
@@ -82,25 +83,55 @@ interface SearchBarViewActionProps {
     closeView: () => void;
     openSearchView: () => void;
     loadRecentQueries: () => string[];
+    moveUp: () => void;
+    moveDown: () => void;
 }
 
 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
 
+const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
+    if (e.keyCode === KEY_CODE_DOWN) {
+        e.preventDefault();
+        if (!props.isPopoverOpen) {
+            props.openSearchView();
+        } else {
+            props.moveDown();
+        }
+    } else if (e.keyCode === KEY_CODE_UP) {
+        e.preventDefault();
+        props.moveUp();
+    } else if (e.keyCode === KEY_CODE_ESC) {
+        e.preventDefault();
+        props.closeView();
+    } else if (e.keyCode === KEY_ENTER) {
+        if (props.currentView === SearchView.BASIC) {
+            e.preventDefault();
+            props.onSearch(props.selectedItem.query);
+        } else if (props.currentView === SearchView.AUTOCOMPLETE) {
+            if (props.selectedItem.id !== props.searchValue) {
+                e.preventDefault();
+                props.navigateTo(props.selectedItem.id);
+            }
+        }
+    }
+};
+
 export const SearchBarView = withStyles(styles)(
     (props : SearchBarViewProps) => {
-        const { classes, isPopoverOpen, closeView, searchValue, openSearchView, onChange, onSubmit } = props;
+        const { classes, isPopoverOpen } = props;
         return (
-            <ClickAwayListener onClickAway={closeView}>
+            <ClickAwayListener onClickAway={props.closeView}>
                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
-                    <form onSubmit={onSubmit}>
+                    <form onSubmit={props.onSubmit}>
                         <Input
                             className={classes.input}
-                            onChange={onChange}
+                            onChange={props.onChange}
                             placeholder="Search"
-                            value={searchValue}
+                            value={props.searchValue}
                             fullWidth={true}
                             disableUnderline={true}
-                            onClick={openSearchView}
+                            onClick={props.openSearchView}
+                            onKeyDown={e => handleKeyDown(e, props)}
                             endAdornment={
                                 <InputAdornment position="end">
                                     <Tooltip title='Search'>
@@ -121,25 +152,25 @@ export const SearchBarView = withStyles(styles)(
 );
 
 const getView = (props: SearchBarViewProps) => {
-    const { onSetView, closeAdvanceView, loadRecentQueries, savedQueries, deleteSavedQuery, searchValue,
-        searchResults, onSearch, navigateTo, editSavedQuery, tags, currentView } = props;
-    switch (currentView) {
+    switch (props.currentView) {
         case SearchView.AUTOCOMPLETE:
             return <SearchBarAutocompleteView
-                navigateTo={navigateTo}
-                searchResults={searchResults}
-                searchValue={searchValue} />;
+                navigateTo={props.navigateTo}
+                searchResults={props.searchResults}
+                searchValue={props.searchValue}
+                selectedItem={props.selectedItem} />;
         case SearchView.ADVANCED:
             return <SearchBarAdvancedView
-                closeAdvanceView={closeAdvanceView}
-                tags={tags} />;
+                closeAdvanceView={props.closeAdvanceView}
+                tags={props.tags} />;
         default:
             return <SearchBarBasicView
-                onSetView={onSetView}
-                onSearch={onSearch}
-                loadRecentQueries={loadRecentQueries}
-                savedQueries={savedQueries}
-                deleteSavedQuery={deleteSavedQuery}
-                editSavedQuery={editSavedQuery} />;
+                onSetView={props.onSetView}
+                onSearch={props.onSearch}
+                loadRecentQueries={props.loadRecentQueries}
+                savedQueries={props.savedQueries}
+                deleteSavedQuery={props.deleteSavedQuery}
+                editSavedQuery={props.editSavedQuery}
+                selectedItem={props.selectedItem} />;
     }
 };
index 68ffecf07171f1733a190731edd0793c46d480e8..e60b214121351d23e47ea36a13e2a707c406c869 100644 (file)
@@ -16,7 +16,7 @@ import {
     navigateToItem,
     editSavedQuery,
     changeData,
-    submitData
+    submitData, moveUp, moveDown
 } from '~/store/search-bar/search-bar-actions';
 import { SearchBarView, SearchBarActionProps, SearchBarDataProps } from '~/views-components/search-bar/search-bar-view';
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
@@ -27,6 +27,7 @@ const mapStateToProps = ({ searchBar, form }: RootState): SearchBarDataProps =>
         currentView: searchBar.currentView,
         isPopoverOpen: searchBar.open,
         searchResults: searchBar.searchResults,
+        selectedItem: searchBar.selectedItem,
         savedQueries: searchBar.savedQueries,
         tags: form.searchBarAdvanceFormName
     };
@@ -43,7 +44,9 @@ const mapDispatchToProps = (dispatch: Dispatch): SearchBarActionProps => ({
     deleteSavedQuery: (id: number) => dispatch<any>(deleteSavedQuery(id)),
     openSearchView: () => dispatch<any>(openSearchView()),
     navigateTo: (uuid: string) => dispatch<any>(navigateToItem(uuid)),
-    editSavedQuery: (data: SearchBarAdvanceFormData) => dispatch<any>(editSavedQuery(data))
+    editSavedQuery: (data: SearchBarAdvanceFormData) => dispatch<any>(editSavedQuery(data)),
+    moveUp: () => dispatch<any>(moveUp()),
+    moveDown: () => dispatch<any>(moveDown())
 });
 
-export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);
\ No newline at end of file
+export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView);