Add arrow navigation in autocomplete view
[arvados-workbench2.git] / src / store / search-bar / search-bar-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { unionize, ofType, UnionOf } from "~/common/unionize";
6 import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
7 import { Dispatch } from 'redux';
8 import { change, arrayPush } from 'redux-form';
9 import { RootState } from '~/store/store';
10 import { initUserProject } from '~/store/tree-picker/tree-picker-actions';
11 import { ServiceRepository } from '~/services/services';
12 import { FilterBuilder } from "~/services/api/filter-builder";
13 import { ResourceKind } from '~/models/resource';
14 import { GroupClass } from '~/models/group';
15 import { SearchView } from '~/store/search-bar/search-bar-reducer';
16 import { navigateToSearchResults, navigateTo } from '~/store/navigation/navigation-action';
17 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
18 import { initialize } from 'redux-form';
19 import { SearchBarAdvanceFormData, PropertyValues } from '~/models/search-bar';
20 import { debounce } from 'debounce';
21
22 export const searchBarActions = unionize({
23     SET_CURRENT_VIEW: ofType<string>(),
24     OPEN_SEARCH_VIEW: ofType<{}>(),
25     CLOSE_SEARCH_VIEW: ofType<{}>(),
26     SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
27     SET_SEARCH_VALUE: ofType<string>(),
28     SET_SAVED_QUERIES: ofType<SearchBarAdvanceFormData[]>(),
29     UPDATE_SAVED_QUERY: ofType<SearchBarAdvanceFormData[]>(),
30     SET_SELECTED_ITEM: ofType<string>(),
31     MOVE_UP: ofType<{}>(),
32     MOVE_DOWN: ofType<{}>()
33 });
34
35 export type SearchBarActions = UnionOf<typeof searchBarActions>;
36
37 export const SEARCH_BAR_ADVANCE_FORM_NAME = 'searchBarAdvanceFormName';
38
39 export const SEARCH_BAR_ADVANCE_FORM_PICKER_ID = 'searchBarAdvanceFormPickerId';
40
41 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
42
43 export const goToView = (currentView: string) => searchBarActions.SET_CURRENT_VIEW(currentView);
44
45 export const saveRecentQuery = (query: string) =>
46     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) =>
47         services.searchService.saveRecentQuery(query);
48
49
50 export const loadRecentQueries = () =>
51     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
52         const recentSearchQueries = services.searchService.getRecentQueries();
53         return recentSearchQueries || [];
54     };
55
56 export const searchData = (searchValue: string) =>
57     async (dispatch: Dispatch, getState: () => RootState) => {
58         const currentView = getState().searchBar.currentView;
59         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
60         // dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
61         if (searchValue.length > 0) {
62             dispatch<any>(searchGroups(searchValue));
63             if (currentView === SearchView.BASIC) {
64                 dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
65                 dispatch(navigateToSearchResults);
66             }
67         }
68     };
69
70 export const searchAdvanceData = (data: SearchBarAdvanceFormData) =>
71     async (dispatch: Dispatch) => {
72         dispatch<any>(saveQuery(data));
73         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
74         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
75         dispatch(navigateToSearchResults);
76     };
77
78 // Todo: create ids for particular searchQuery
79 const saveQuery = (data: SearchBarAdvanceFormData) =>
80     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
81         const savedSearchQueries = services.searchService.getSavedQueries();
82         const filteredQuery = savedSearchQueries.find(query => query.searchQuery === data.searchQuery);
83         if (data.saveQuery && data.searchQuery) {
84             if (filteredQuery) {
85                 services.searchService.editSavedQueries(data);
86                 dispatch(searchBarActions.UPDATE_SAVED_QUERY(savedSearchQueries));
87                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully updated', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
88             } else {
89                 services.searchService.saveQuery(data);
90                 dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
91                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully saved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
92             }
93         }
94     };
95
96 export const deleteSavedQuery = (id: number) =>
97     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
98         services.searchService.deleteSavedQuery(id);
99         const savedSearchQueries = services.searchService.getSavedQueries();
100         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
101         return savedSearchQueries || [];
102     };
103
104 export const editSavedQuery = (data: SearchBarAdvanceFormData) =>
105     (dispatch: Dispatch<any>) => {
106         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED));
107         dispatch(searchBarActions.SET_SEARCH_VALUE(data.searchQuery));
108         dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
109     };
110
111 export const openSearchView = () =>
112     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
113         dispatch(searchBarActions.OPEN_SEARCH_VIEW());
114         const savedSearchQueries = services.searchService.getSavedQueries();
115         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
116     };
117
118 export const closeSearchView = () =>
119     (dispatch: Dispatch<any>, getState: () => RootState) => {
120         const isOpen = getState().searchBar.open;
121         if (isOpen) {
122             dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
123             dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
124         }
125     };
126
127 export const closeAdvanceView = () =>
128     (dispatch: Dispatch<any>) => {
129         dispatch(searchBarActions.SET_SEARCH_VALUE(''));
130         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
131     };
132
133 export const navigateToItem = (uuid: string) =>
134     (dispatch: Dispatch<any>) => {
135         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
136         dispatch(navigateTo(uuid));
137     };
138
139 export const changeData = (searchValue: string) =>
140     (dispatch: Dispatch, getState: () => RootState) => {
141         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
142         const currentView = getState().searchBar.currentView;
143         const searchValuePresent = searchValue.length > 0;
144
145         if (currentView === SearchView.ADVANCED) {
146
147         } else if (searchValuePresent) {
148             dispatch<any>(goToView(SearchView.AUTOCOMPLETE));
149             debounceStartSearch(dispatch);
150         } else {
151             dispatch<any>(goToView(SearchView.BASIC));
152             dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
153             dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
154         }
155     };
156
157 export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
158     (dispatch: Dispatch, getState: () => RootState) => {
159         event.preventDefault();
160         const searchValue = getState().searchBar.searchValue;
161         dispatch<any>(saveRecentQuery(searchValue));
162         dispatch<any>(loadRecentQueries());
163         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
164         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
165         dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
166         dispatch(navigateToSearchResults);
167     };
168
169 const debounceStartSearch = debounce((dispatch: Dispatch) => dispatch<any>(startSearch()), DEFAULT_SEARCH_DEBOUNCE);
170
171 const startSearch = () =>
172     (dispatch: Dispatch, getState: () => RootState) => {
173         const searchValue = getState().searchBar.searchValue;
174         dispatch<any>(searchData(searchValue));
175     };
176
177 const searchGroups = (searchValue: string, limit = 5, resourceKind?: ResourceKind) =>
178     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
179         const currentView = getState().searchBar.currentView;
180
181         if (searchValue || currentView === SearchView.ADVANCED) {
182             const filters = getFilters('name', searchValue, resourceKind);
183             const { items } = await services.groupsService.contents('', {
184                 filters,
185                 limit,
186                 recursive: true
187             });
188             dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
189         }
190     };
191
192 export const getFilters = (filterName: string, searchValue: string, resourceKind?: ResourceKind): string => {
193     return new FilterBuilder()
194         .addIsA("uuid", buildUuidFilter(resourceKind))
195         .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION)
196         .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS)
197         .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT)
198         .addEqual('groupClass', GroupClass.PROJECT, GroupContentsResourcePrefix.PROJECT)
199         .getFilters();
200 };
201
202 const buildUuidFilter = (type?: ResourceKind): ResourceKind[] => {
203     return type ? [type] : [ResourceKind.PROJECT, ResourceKind.COLLECTION, ResourceKind.PROCESS];
204 };
205
206 export const initAdvanceFormProjectsTree = () =>
207     (dispatch: Dispatch) => {
208         dispatch<any>(initUserProject(SEARCH_BAR_ADVANCE_FORM_PICKER_ID));
209     };
210
211 export const changeAdvanceFormProperty = (property: string, value: PropertyValues[] | string = '') =>
212     (dispatch: Dispatch) => {
213         dispatch(change(SEARCH_BAR_ADVANCE_FORM_NAME, property, value));
214     };
215
216 export const updateAdvanceFormProperties = (propertyValues: PropertyValues) =>
217     (dispatch: Dispatch) => {
218         dispatch(arrayPush(SEARCH_BAR_ADVANCE_FORM_NAME, 'properties', propertyValues));
219     };
220
221 export const moveUp = () =>
222     (dispatch: Dispatch) => {
223         dispatch(searchBarActions.MOVE_UP());
224     };
225
226 export const moveDown = () =>
227     (dispatch: Dispatch) => {
228         dispatch(searchBarActions.MOVE_DOWN());
229     };