Merge branch '14917-searching-by-properties'
[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 { ofType, unionize, UnionOf } from "~/common/unionize";
6 import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
7 import { Dispatch } from 'redux';
8 import { arrayPush, change, initialize } from 'redux-form';
9 import { RootState } from '~/store/store';
10 import { initUserProject, treePickerActions } from '~/store/tree-picker/tree-picker-actions';
11 import { ServiceRepository } from '~/services/services';
12 import { FilterBuilder } from "~/services/api/filter-builder";
13 import { ResourceKind, isResourceUuid, extractUuidKind } from '~/models/resource';
14 import { SearchView } from '~/store/search-bar/search-bar-reducer';
15 import { navigateTo, navigateToSearchResults } from '~/store/navigation/navigation-action';
16 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
17 import { PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar';
18 import { debounce } from 'debounce';
19 import * as _ from "lodash";
20 import { getModifiedKeysValues } from "~/common/objects";
21 import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-actions";
22 import { Session } from "~/models/session";
23 import { searchResultsPanelActions } from "~/store/search-results-panel/search-results-panel-actions";
24 import { ListResults } from "~/services/common-service/common-service";
25
26 export const searchBarActions = unionize({
27     SET_CURRENT_VIEW: ofType<string>(),
28     OPEN_SEARCH_VIEW: ofType<{}>(),
29     CLOSE_SEARCH_VIEW: ofType<{}>(),
30     SET_SEARCH_RESULTS: ofType<GroupContentsResource[]>(),
31     SET_SEARCH_VALUE: ofType<string>(),
32     SET_SAVED_QUERIES: ofType<SearchBarAdvanceFormData[]>(),
33     SET_RECENT_QUERIES: ofType<string[]>(),
34     UPDATE_SAVED_QUERY: ofType<SearchBarAdvanceFormData[]>(),
35     SET_SELECTED_ITEM: ofType<string>(),
36     MOVE_UP: ofType<{}>(),
37     MOVE_DOWN: ofType<{}>(),
38     SELECT_FIRST_ITEM: ofType<{}>()
39 });
40
41 export type SearchBarActions = UnionOf<typeof searchBarActions>;
42
43 export const SEARCH_BAR_ADVANCE_FORM_NAME = 'searchBarAdvanceFormName';
44
45 export const SEARCH_BAR_ADVANCE_FORM_PICKER_ID = 'searchBarAdvanceFormPickerId';
46
47 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
48
49 export const goToView = (currentView: string) => searchBarActions.SET_CURRENT_VIEW(currentView);
50
51 export const saveRecentQuery = (query: string) =>
52     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) =>
53         services.searchService.saveRecentQuery(query);
54
55
56 export const loadRecentQueries = () =>
57     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
58         const recentQueries = services.searchService.getRecentQueries();
59         dispatch(searchBarActions.SET_RECENT_QUERIES(recentQueries));
60         return recentQueries;
61     };
62
63 export const searchData = (searchValue: string) =>
64     async (dispatch: Dispatch, getState: () => RootState) => {
65         const currentView = getState().searchBar.currentView;
66         dispatch(searchResultsPanelActions.CLEAR());
67         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
68         if (searchValue.length > 0) {
69             dispatch<any>(searchGroups(searchValue, 5));
70             if (currentView === SearchView.BASIC) {
71                 dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
72                 dispatch(navigateToSearchResults);
73             }
74         }
75     };
76
77 export const searchAdvanceData = (data: SearchBarAdvanceFormData) =>
78     async (dispatch: Dispatch) => {
79         dispatch<any>(saveQuery(data));
80         dispatch(searchResultsPanelActions.CLEAR());
81         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
82         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
83         dispatch(navigateToSearchResults);
84     };
85
86 export const setSearchValueFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) =>
87     (dispatch: Dispatch, getState: () => RootState) => {
88         const searchValue = getState().searchBar.searchValue;
89         const value = getQueryFromAdvancedData({
90             ...data,
91             searchValue
92         }, prevData);
93         dispatch(searchBarActions.SET_SEARCH_VALUE(value));
94     };
95
96 export const setAdvancedDataFromSearchValue = (search: string) =>
97     async (dispatch: Dispatch) => {
98         const data = getAdvancedDataFromQuery(search);
99         dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
100         if (data.projectUuid) {
101             await dispatch<any>(activateSearchBarProject(data.projectUuid));
102             dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID, id: data.projectUuid }));
103         }
104     };
105
106 const saveQuery = (data: SearchBarAdvanceFormData) =>
107     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
108         const savedQueries = services.searchService.getSavedQueries();
109         if (data.saveQuery && data.queryName) {
110             const filteredQuery = savedQueries.find(query => query.queryName === data.queryName);
111             data.searchValue = getState().searchBar.searchValue;
112             if (filteredQuery) {
113                 services.searchService.editSavedQueries(data);
114                 dispatch(searchBarActions.UPDATE_SAVED_QUERY(savedQueries));
115                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully updated', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
116             } else {
117                 services.searchService.saveQuery(data);
118                 dispatch(searchBarActions.SET_SAVED_QUERIES(savedQueries));
119                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Query has been successfully saved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
120             }
121         }
122     };
123
124 export const deleteSavedQuery = (id: number) =>
125     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
126         services.searchService.deleteSavedQuery(id);
127         const savedSearchQueries = services.searchService.getSavedQueries();
128         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
129         return savedSearchQueries || [];
130     };
131
132 export const editSavedQuery = (data: SearchBarAdvanceFormData) =>
133     (dispatch: Dispatch<any>) => {
134         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED));
135         dispatch(searchBarActions.SET_SEARCH_VALUE(getQueryFromAdvancedData(data)));
136         dispatch<any>(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data));
137     };
138
139 export const openSearchView = () =>
140     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
141         const savedSearchQueries = services.searchService.getSavedQueries();
142         dispatch(searchBarActions.SET_SAVED_QUERIES(savedSearchQueries));
143         dispatch(loadRecentQueries());
144         dispatch(searchBarActions.OPEN_SEARCH_VIEW());
145         dispatch(searchBarActions.SELECT_FIRST_ITEM());
146     };
147
148 export const closeSearchView = () =>
149     (dispatch: Dispatch<any>) => {
150         dispatch(searchBarActions.SET_SELECTED_ITEM(''));
151         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
152     };
153
154 export const closeAdvanceView = () =>
155     (dispatch: Dispatch<any>) => {
156         dispatch(searchBarActions.SET_SEARCH_VALUE(''));
157         dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID }));
158         dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
159     };
160
161 export const navigateToItem = (uuid: string) =>
162     (dispatch: Dispatch<any>) => {
163         dispatch(searchBarActions.SET_SELECTED_ITEM(''));
164         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
165         dispatch(navigateTo(uuid));
166     };
167
168 export const changeData = (searchValue: string) =>
169     (dispatch: Dispatch, getState: () => RootState) => {
170         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
171         const currentView = getState().searchBar.currentView;
172         const searchValuePresent = searchValue.length > 0;
173
174         if (currentView === SearchView.ADVANCED) {
175             dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.AUTOCOMPLETE));
176         } else if (searchValuePresent) {
177             dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.AUTOCOMPLETE));
178             dispatch(searchBarActions.SET_SELECTED_ITEM(searchValue));
179             debounceStartSearch(dispatch);
180         } else {
181             dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC));
182             dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
183             dispatch(searchBarActions.SELECT_FIRST_ITEM());
184         }
185     };
186
187 export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
188     (dispatch: Dispatch, getState: () => RootState) => {
189         event.preventDefault();
190         const searchValue = getState().searchBar.searchValue;
191         dispatch<any>(saveRecentQuery(searchValue));
192         dispatch<any>(loadRecentQueries());
193         dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
194         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
195         dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
196         dispatch(searchResultsPanelActions.CLEAR());
197         dispatch(navigateToSearchResults);
198     };
199
200 const debounceStartSearch = debounce((dispatch: Dispatch) => dispatch<any>(startSearch()), DEFAULT_SEARCH_DEBOUNCE);
201
202 const startSearch = () =>
203     (dispatch: Dispatch, getState: () => RootState) => {
204         const searchValue = getState().searchBar.searchValue;
205         dispatch<any>(searchData(searchValue));
206     };
207
208 const searchGroups = (searchValue: string, limit: number) =>
209     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
210         const currentView = getState().searchBar.currentView;
211
212         if (searchValue || currentView === SearchView.ADVANCED) {
213             const sq = parseSearchQuery(searchValue);
214             const clusterId = getSearchQueryFirstProp(sq, 'cluster');
215             const sessions = getSearchSessions(clusterId, getState().auth.sessions);
216             const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session => {
217                 const filters = searchQueryToFilters(sq);
218                 return services.groupsService.contents('', {
219                     filters,
220                     limit,
221                     recursive: true
222                 }, session);
223             }));
224
225             const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
226             dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
227         }
228     };
229
230 const buildQueryFromKeyMap = (data: any, keyMap: string[][], mode: 'rebuild' | 'reuse') => {
231     let value = data.searchValue;
232
233     const addRem = (field: string, key: string) => {
234         const v = data[key];
235
236         if (data.hasOwnProperty(key)) {
237             const pattern = v === false
238                 ? `${field.replace(':', '\\:\\s*')}\\s*`
239                 : `${field.replace(':', '\\:\\s*')}\\:\\s*[\\w|\\#|\\-|\\/]*\\s*`;
240             value = value.replace(new RegExp(pattern), '');
241         }
242
243         if (v) {
244             const nv = v === true
245                 ? `${field}`
246                 : `${field}:${v}`;
247
248             if (mode === 'rebuild') {
249                 value = value + ' ' + nv;
250             } else {
251                 value = nv + ' ' + value;
252             }
253         }
254     };
255
256     keyMap.forEach(km => addRem(km[0], km[1]));
257
258     return value;
259 };
260
261 export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) => {
262     let value = '';
263
264     const flatData = (data: SearchBarAdvanceFormData) => {
265         const fo = {
266             searchValue: data.searchValue,
267             type: data.type,
268             cluster: data.cluster,
269             projectUuid: data.projectUuid,
270             inTrash: data.inTrash,
271             dateFrom: data.dateFrom,
272             dateTo: data.dateTo,
273         };
274         (data.properties || []).forEach(p => fo[`prop-${p.key}`] = p.value);
275         return fo;
276     };
277
278     const keyMap = [
279         ['type', 'type'],
280         ['cluster', 'cluster'],
281         ['project', 'projectUuid'],
282         ['is:trashed', 'inTrash'],
283         ['from', 'dateFrom'],
284         ['to', 'dateTo']
285     ];
286     _.union(data.properties, prevData ? prevData.properties : [])
287         .forEach(p => keyMap.push([`has:${p.key}`, `prop-${p.key}`]));
288
289     if (prevData) {
290         const obj = getModifiedKeysValues(flatData(data), flatData(prevData));
291         value = buildQueryFromKeyMap({
292             searchValue: data.searchValue,
293             ...obj
294         } as SearchBarAdvanceFormData, keyMap, "reuse");
295     } else {
296         value = buildQueryFromKeyMap(flatData(data), keyMap, "rebuild");
297     }
298
299     value = value.trim();
300     return value;
301 };
302
303 export class ParseSearchQuery {
304     hasKeywords: boolean;
305     values: string[];
306     properties: {
307         [key: string]: string[]
308     };
309 }
310
311 export const parseSearchQuery: (query: string) => ParseSearchQuery = (searchValue: string) => {
312     const keywords = [
313         'type:',
314         'cluster:',
315         'project:',
316         'is:',
317         'from:',
318         'to:',
319         'has:'
320     ];
321
322     const hasKeywords = (search: string) => keywords.reduce((acc, keyword) => acc + (search.includes(keyword) ? 1 : 0), 0);
323     let keywordsCnt = 0;
324
325     const properties = {};
326
327     keywords.forEach(k => {
328         if (k) {
329             let p = searchValue.indexOf(k);
330             const key = k.substr(0, k.length - 1);
331
332             while (p >= 0) {
333                 const l = searchValue.length;
334                 keywordsCnt += 1;
335
336                 let v = '';
337                 let i = p + k.length;
338                 while (i < l && searchValue[i] === ' ') {
339                     ++i;
340                 }
341                 const vp = i;
342                 while (i < l && searchValue[i] !== ' ') {
343                     v += searchValue[i];
344                     ++i;
345                 }
346
347                 if (hasKeywords(v)) {
348                     searchValue = searchValue.substr(0, p) + searchValue.substr(vp);
349                 } else {
350                     if (v !== '') {
351                         if (!properties[key]) {
352                             properties[key] = [];
353                         }
354                         properties[key].push(v);
355                     }
356                     searchValue = searchValue.substr(0, p) + searchValue.substr(i);
357                 }
358                 p = searchValue.indexOf(k);
359             }
360         }
361     });
362
363     const values = _.uniq(searchValue.split(' ').filter(v => v.length > 0));
364
365     return { hasKeywords: keywordsCnt > 0, values, properties };
366 };
367
368 export const getSearchQueryFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0];
369 export const getSearchQueryPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value);
370 export const getSearchQueryProperties = (sq: ParseSearchQuery): PropertyValue[] => {
371     if (sq.properties.has) {
372         return sq.properties.has.map((value: string) => {
373             const v = value.split(':');
374             return {
375                 key: v[0],
376                 value: v[1]
377             };
378         });
379     }
380     return [];
381 };
382
383 export const getAdvancedDataFromQuery = (query: string): SearchBarAdvanceFormData => {
384     const sq = parseSearchQuery(query);
385
386     return {
387         searchValue: sq.values.join(' '),
388         type: getSearchQueryFirstProp(sq, 'type') as ResourceKind,
389         cluster: getSearchQueryFirstProp(sq, 'cluster'),
390         projectUuid: getSearchQueryFirstProp(sq, 'project'),
391         inTrash: getSearchQueryPropValue(sq, 'is', 'trashed') !== undefined,
392         dateFrom: getSearchQueryFirstProp(sq, 'from'),
393         dateTo: getSearchQueryFirstProp(sq, 'to'),
394         properties: getSearchQueryProperties(sq),
395         saveQuery: false,
396         queryName: ''
397     };
398 };
399
400 export const getSearchSessions = (clusterId: string | undefined, sessions: Session[]): Session[] => {
401     return sessions.filter(s => s.loggedIn && (!clusterId || s.clusterId === clusterId));
402 };
403
404 export const getFilters = (filterName: string, searchValue: string, sq: ParseSearchQuery): string => {
405     const filter = new FilterBuilder();
406     const isSearchQueryUuid = isResourceUuid(sq.values[0]);
407     const resourceKind = getSearchQueryFirstProp(sq, 'type') as ResourceKind;
408
409     let prefix = '';
410     switch (resourceKind) {
411         case ResourceKind.COLLECTION:
412             prefix = GroupContentsResourcePrefix.COLLECTION;
413             break;
414         case ResourceKind.PROCESS:
415             prefix = GroupContentsResourcePrefix.PROCESS;
416             break;
417         default:
418             prefix = GroupContentsResourcePrefix.PROJECT;
419             break;
420     }
421
422     const isTrashed = getSearchQueryPropValue(sq, 'is', 'trashed');
423
424     if (!sq.hasKeywords && !isSearchQueryUuid) {
425         filter
426             .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION)
427             .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT)
428             .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS)
429             .addEqual('is_trashed', false, GroupContentsResourcePrefix.COLLECTION)
430             .addEqual('is_trashed', false, GroupContentsResourcePrefix.PROJECT);
431
432         if (isTrashed) {
433             filter.addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS);
434         }
435     } else if (!sq.hasKeywords && isSearchQueryUuid) {
436         filter
437             .addILike('uuid', searchValue, GroupContentsResourcePrefix.COLLECTION)
438             .addILike('uuid', searchValue, GroupContentsResourcePrefix.PROJECT)
439             .addILike('uuid', searchValue, GroupContentsResourcePrefix.PROCESS)
440             .addEqual('is_trashed', false, GroupContentsResourcePrefix.COLLECTION)
441             .addEqual('is_trashed', false, GroupContentsResourcePrefix.PROJECT);
442     }
443     else {
444         if (prefix) {
445             sq.values.forEach(v =>
446                 filter.addILike(filterName, v, prefix)
447             );
448         } else {
449             sq.values.forEach(v => {
450                 filter
451                     .addILike(filterName, v, GroupContentsResourcePrefix.COLLECTION)
452                     .addILike(filterName, v, GroupContentsResourcePrefix.PROJECT)
453                     .addILike(filterName, v, GroupContentsResourcePrefix.PROCESS)
454                     .addEqual('is_trashed', false, GroupContentsResourcePrefix.COLLECTION)
455                     .addEqual('is_trashed', false, GroupContentsResourcePrefix.PROJECT);
456
457                 if (isTrashed) {
458                     filter.addILike(filterName, v, GroupContentsResourcePrefix.PROCESS);
459                 }
460             });
461         }
462         if (prefix && !isTrashed) {
463             sq.values.forEach(v =>
464                 filter.addILike(filterName, v, prefix)
465                     .addEqual('is_trashed', false, GroupContentsResourcePrefix.COLLECTION)
466                     .addEqual('is_trashed', false, GroupContentsResourcePrefix.PROJECT)
467             );
468         }
469
470         if (isTrashed) {
471             sq.values.forEach(v => {
472                 filter.addEqual('is_trashed', true, GroupContentsResourcePrefix.COLLECTION)
473                     .addEqual('is_trashed', true, GroupContentsResourcePrefix.PROJECT)
474                     .addILike(filterName, v, GroupContentsResourcePrefix.COLLECTION)
475                     .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS);
476             });
477         }
478
479         const projectUuid = getSearchQueryFirstProp(sq, 'project');
480         if (projectUuid) {
481             filter.addEqual('uuid', projectUuid, prefix);
482         }
483
484         const dateFrom = getSearchQueryFirstProp(sq, 'from');
485         if (dateFrom) {
486             filter.addGte('modified_at', buildDateFilter(dateFrom));
487         }
488
489         const dateTo = getSearchQueryFirstProp(sq, 'to');
490         if (dateTo) {
491             filter.addLte('modified_at', buildDateFilter(dateTo));
492         }
493
494         const props = getSearchQueryProperties(sq);
495         props.forEach(p => {
496             if (p.value) {
497                 filter.addILike(`properties.${p.key}`, p.value, GroupContentsResourcePrefix.PROJECT)
498                     .addILike(`properties.${p.key}`, p.value, GroupContentsResourcePrefix.COLLECTION);
499             }
500             filter.addExists(p.key);
501         });
502     }
503
504     return filter
505         .addIsA("uuid", buildUuidFilter(resourceKind))
506         .getFilters();
507 };
508
509 export const searchQueryToFilters = (sq: ParseSearchQuery): string => {
510     const filter = new FilterBuilder();
511     const resourceKind = getSearchQueryFirstProp(sq, 'type') as ResourceKind;
512
513     const projectUuid = getSearchQueryFirstProp(sq, 'project');
514     if (projectUuid) {
515         filter.addEqual('ownerUuid', projectUuid);
516     }
517
518     const dateFrom = getSearchQueryFirstProp(sq, 'from');
519     if (dateFrom) {
520         filter.addGte('modified_at', buildDateFilter(dateFrom));
521     }
522
523     const dateTo = getSearchQueryFirstProp(sq, 'to');
524     if (dateTo) {
525         filter.addLte('modified_at', buildDateFilter(dateTo));
526     }
527
528     const props = getSearchQueryProperties(sq);
529     props.forEach(p => {
530         if (p.value) {
531             filter
532                 .addILike(`properties.${p.key}`, p.value, GroupContentsResourcePrefix.PROJECT)
533                 .addILike(`properties.${p.key}`, p.value, GroupContentsResourcePrefix.COLLECTION);
534         }
535         filter.addExists(p.key);
536     });
537
538     return filter
539         .addIsA("uuid", buildUuidFilter(resourceKind))
540         .addFullTextSearch(sq.values.join(' '))
541         .getFilters();
542 };
543
544 const buildUuidFilter = (type?: ResourceKind): ResourceKind[] => {
545     return type ? [type] : [ResourceKind.PROJECT, ResourceKind.COLLECTION, ResourceKind.PROCESS];
546 };
547
548 const buildDateFilter = (date?: string): string => {
549     return date ? date : '';
550 };
551
552 export const initAdvanceFormProjectsTree = () =>
553     (dispatch: Dispatch) => {
554         dispatch<any>(initUserProject(SEARCH_BAR_ADVANCE_FORM_PICKER_ID));
555     };
556
557 export const changeAdvanceFormProperty = (property: string, value: PropertyValue[] | string = '') =>
558     (dispatch: Dispatch) => {
559         dispatch(change(SEARCH_BAR_ADVANCE_FORM_NAME, property, value));
560     };
561
562 export const updateAdvanceFormProperties = (propertyValues: PropertyValue) =>
563     (dispatch: Dispatch) => {
564         dispatch(arrayPush(SEARCH_BAR_ADVANCE_FORM_NAME, 'properties', propertyValues));
565     };
566
567 export const moveUp = () =>
568     (dispatch: Dispatch) => {
569         dispatch(searchBarActions.MOVE_UP());
570     };
571
572 export const moveDown = () =>
573     (dispatch: Dispatch) => {
574         dispatch(searchBarActions.MOVE_DOWN());
575     };