From: Daniel Kos Date: Thu, 6 Dec 2018 07:47:00 +0000 (+0100) Subject: refs #14280 Merge branch 'origin/14280-query-language' X-Git-Tag: 1.4.0~95 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/f83344568cb070f716527288abe88a4ec5b0c305?hp=ceda57340f34d71fb4289b344e6ca839db06f5e7 refs #14280 Merge branch 'origin/14280-query-language' Arvados-DCO-1.1-Signed-off-by: Daniel Kos --- diff --git a/package.json b/package.json index 623b86d3..13326304 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@types/react-dnd": "3.0.2", "@types/react-dropzone": "4.2.2", "@types/react-highlight-words": "0.12.0", - "@types/redux-form": "7.4.5", + "@types/redux-form": "7.4.12", "@types/reselect": "2.2.0", "@types/shell-quote": "1.6.0", "axios": "0.18.0", diff --git a/src/common/formatters.ts b/src/common/formatters.ts index e2097878..5383c66e 100644 --- a/src/common/formatters.ts +++ b/src/common/formatters.ts @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0 +import { PropertyValue } from "~/models/search-bar"; + export const formatDate = (isoDate?: string) => { if (isoDate) { const date = new Date(isoDate); @@ -67,3 +69,12 @@ const FILE_SIZES = [ unit: "B" } ]; + +export const formatPropertyValue = (pv: PropertyValue) => { + if (pv.key) { + return pv.value + ? `${pv.key}: ${pv.value}` + : pv.key; + } + return ""; +}; diff --git a/src/common/objects.ts b/src/common/objects.ts new file mode 100644 index 00000000..0a01ed62 --- /dev/null +++ b/src/common/objects.ts @@ -0,0 +1,18 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 +import * as _ from "lodash"; + +export function getModifiedKeys(a: any, b: any) { + const keys = _.union(_.keys(a), _.keys(b)); + return _.filter(keys, key => a[key] !== b[key]); +} + +export function getModifiedKeysValues(a: any, b: any) { + const keys = getModifiedKeys(a, b); + const obj = {}; + keys.forEach(k => { + obj[k] = a[k]; + }); + return obj; +} diff --git a/src/models/search-bar.ts b/src/models/search-bar.ts index 4df5c38f..798f9c8f 100644 --- a/src/models/search-bar.ts +++ b/src/models/search-bar.ts @@ -12,11 +12,12 @@ export type SearchBarAdvanceFormData = { dateFrom: string; dateTo: string; saveQuery: boolean; - searchQuery: string; - properties: PropertyValues[]; -} & PropertyValues; + queryName: string; + searchValue: string; + properties: PropertyValue[]; +}; -export interface PropertyValues { +export interface PropertyValue { key: string; value: string; } @@ -25,4 +26,4 @@ export enum ClusterObjectType { INDIANAPOLIS = "indianapolis", KAISERAUGST = "kaiseraugst", PENZBERG = "penzberg" -} \ No newline at end of file +} diff --git a/src/services/api/filter-builder.ts b/src/services/api/filter-builder.ts index 1ebf4886..08746c81 100644 --- a/src/services/api/filter-builder.ts +++ b/src/services/api/filter-builder.ts @@ -51,6 +51,10 @@ export class FilterBuilder { return this.addCondition(field, "<=", value, "", "", resourcePrefix); } + public addExists(value?: string, resourcePrefix?: string) { + return this.addCondition("properties", "exists", value, "", "", resourcePrefix); + } + public getFilters() { return this.filters; } @@ -69,7 +73,9 @@ export class FilterBuilder { ? resourcePrefix + "." : ""; - this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`; + const fld = field.indexOf('properties.') < 0 ? _.snakeCase(field) : field; + + this.filters += `${this.filters ? "," : ""}["${resPrefix}${fld}","${cond}",${value}]`; } return this; } diff --git a/src/services/search-service/search-service.ts b/src/services/search-service/search-service.ts index a8e91c39..84d120a8 100644 --- a/src/services/search-service/search-service.ts +++ b/src/services/search-service/search-service.ts @@ -26,7 +26,7 @@ export class SearchService { } editSavedQueries(data: SearchBarAdvanceFormData) { - const itemIndex = this.savedQueries.findIndex(item => item.searchQuery === data.searchQuery); + const itemIndex = this.savedQueries.findIndex(item => item.queryName === data.queryName); this.savedQueries[itemIndex] = {...data}; localStorage.setItem('savedQueries', JSON.stringify(this.savedQueries)); } diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts index e1b36f82..4ed34875 100644 --- a/src/store/auth/auth-action.ts +++ b/src/store/auth/auth-action.ts @@ -4,7 +4,7 @@ import { ofType, unionize, UnionOf } from '~/common/unionize'; import { Dispatch } from "redux"; -import { reset, stopSubmit, startSubmit } from 'redux-form'; +import { reset, stopSubmit, startSubmit, FormErrors } from 'redux-form'; import { AxiosInstance } from "axios"; import { RootState } from "../store"; import { snackbarActions } from '~/store/snackbar/snackbar-actions'; @@ -143,9 +143,9 @@ export const createSshKey = (data: SshKeyCreateFormDialogData) => } catch (e) { const error = getAuthorizedKeysServiceError(e); if (error === AuthorizedKeysServiceError.UNIQUE_PUBLIC_KEY) { - dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key already exists.' })); + dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key already exists.' } as FormErrors)); } else if (error === AuthorizedKeysServiceError.INVALID_PUBLIC_KEY) { - dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key is invalid' })); + dispatch(stopSubmit(SSH_KEY_CREATE_FORM_NAME, { publicKey: 'Public key is invalid' } as FormErrors)); } } }; diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts index 3f82d29e..11cdd622 100644 --- a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts +++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts @@ -11,7 +11,7 @@ import { snackbarActions, SnackbarKind } from "../../snackbar/snackbar-actions"; import { dialogActions } from '../../dialog/dialog-actions'; import { getNodeValue } from "~/models/tree"; import { filterCollectionFilesBySelection } from './collection-panel-files-state'; -import { startSubmit, stopSubmit, reset, initialize } from 'redux-form'; +import { startSubmit, stopSubmit, reset, initialize, FormErrors } from 'redux-form'; import { getDialog } from "~/store/dialog/dialog-reducer"; import { getFileFullPath } from "~/services/collection-service/collection-service-files-response"; import { resourcesDataActions } from "~/store/resources-data/resources-data-actions"; @@ -130,7 +130,10 @@ export const renameFile = (newName: string) => dispatch(dialogActions.CLOSE_DIALOG({ id: RENAME_FILE_DIALOG })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File name changed.', hideDuration: 2000 })); } catch (e) { - dispatch(stopSubmit(RENAME_FILE_DIALOG, { name: 'Could not rename the file' })); + const errors: FormErrors = { + name: 'Could not rename the file' + }; + dispatch(stopSubmit(RENAME_FILE_DIALOG, errors)); } } } diff --git a/src/store/collections/collection-copy-actions.ts b/src/store/collections/collection-copy-actions.ts index e5a6676c..0ce92dfa 100644 --- a/src/store/collections/collection-copy-actions.ts +++ b/src/store/collections/collection-copy-actions.ts @@ -4,7 +4,7 @@ import { Dispatch } from "redux"; import { dialogActions } from "~/store/dialog/dialog-actions"; -import { initialize, startSubmit, stopSubmit } from 'redux-form'; +import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions'; import { RootState } from '~/store/store'; import { ServiceRepository } from '~/services/services'; @@ -39,7 +39,10 @@ export const copyCollection = (resource: CopyFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(COLLECTION_COPY_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' })); + dispatch(stopSubmit( + COLLECTION_COPY_FORM_NAME, + { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors + )); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME })); throw new Error('Could not copy the collection.'); diff --git a/src/store/collections/collection-create-actions.ts b/src/store/collections/collection-create-actions.ts index f4375688..8d1e9ba5 100644 --- a/src/store/collections/collection-create-actions.ts +++ b/src/store/collections/collection-create-actions.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { reset, startSubmit, stopSubmit, initialize } from 'redux-form'; +import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form'; import { RootState } from '~/store/store'; import { dialogActions } from "~/store/dialog/dialog-actions"; import { ServiceRepository } from '~/services/services'; @@ -40,7 +40,7 @@ export const openCollectionCreateDialog = (ownerUuid: string) => export const createCollection = (data: CollectionCreateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME)); - let newCollection: CollectionResource | null = null; + let newCollection: CollectionResource; try { dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME)); newCollection = await services.collectionService.create(data); @@ -52,7 +52,7 @@ export const createCollection = (data: CollectionCreateFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' })); + dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors)); } else if (error === CommonResourceServiceError.NONE) { dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME)); dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME })); diff --git a/src/store/collections/collection-move-actions.ts b/src/store/collections/collection-move-actions.ts index 770eed1a..aacaf4e6 100644 --- a/src/store/collections/collection-move-actions.ts +++ b/src/store/collections/collection-move-actions.ts @@ -4,7 +4,7 @@ import { Dispatch } from "redux"; import { dialogActions } from "~/store/dialog/dialog-actions"; -import { startSubmit, stopSubmit, initialize } from 'redux-form'; +import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form'; import { ServiceRepository } from '~/services/services'; import { RootState } from '~/store/store'; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; @@ -39,7 +39,7 @@ export const moveCollection = (resource: MoveToFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' })); + dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors)); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 })); diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts index b9ada5ee..ef2c1284 100644 --- a/src/store/collections/collection-partial-copy-actions.ts +++ b/src/store/collections/collection-partial-copy-actions.ts @@ -4,7 +4,7 @@ import { Dispatch } from 'redux'; import { RootState } from '~/store/store'; -import { initialize, startSubmit, stopSubmit } from 'redux-form'; +import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions'; import { dialogActions } from '~/store/dialog/dialog-actions'; import { ServiceRepository } from '~/services/services'; @@ -67,7 +67,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME, { name: 'Collection with this name already exists.' })); + dispatch(stopSubmit(COLLECTION_PARTIAL_COPY_FORM_NAME, { name: 'Collection with this name already exists.' } as FormErrors)); } else if (error === CommonResourceServiceError.UNKNOWN) { dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not create a copy of collection', hideDuration: 2000 })); diff --git a/src/store/collections/collection-update-actions.ts b/src/store/collections/collection-update-actions.ts index 9c859234..02ec8bb5 100644 --- a/src/store/collections/collection-update-actions.ts +++ b/src/store/collections/collection-update-actions.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { initialize, startSubmit, stopSubmit } from 'redux-form'; +import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; import { RootState } from "~/store/store"; import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action"; import { dialogActions } from "~/store/dialog/dialog-actions"; @@ -41,7 +41,7 @@ export const updateCollection = (collection: Partial) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' })); + dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors)); } dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME)); return; diff --git a/src/store/processes/process-move-actions.ts b/src/store/processes/process-move-actions.ts index 7e65bcca..dcf97185 100644 --- a/src/store/processes/process-move-actions.ts +++ b/src/store/processes/process-move-actions.ts @@ -4,7 +4,7 @@ import { Dispatch } from "redux"; import { dialogActions } from "~/store/dialog/dialog-actions"; -import { startSubmit, stopSubmit, initialize } from 'redux-form'; +import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form'; import { ServiceRepository } from '~/services/services'; import { RootState } from '~/store/store'; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; @@ -42,11 +42,11 @@ export const moveProcess = (resource: MoveToFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' })); + dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' } as FormErrors)); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000 })); } return; } - }; \ No newline at end of file + }; diff --git a/src/store/processes/process-update-actions.ts b/src/store/processes/process-update-actions.ts index 2063f113..372e1882 100644 --- a/src/store/processes/process-update-actions.ts +++ b/src/store/processes/process-update-actions.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { initialize, startSubmit, stopSubmit } from 'redux-form'; +import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; import { RootState } from "~/store/store"; import { dialogActions } from "~/store/dialog/dialog-actions"; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; @@ -41,11 +41,11 @@ export const updateProcess = (resource: ProcessUpdateFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' })); + dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' } as FormErrors)); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME })); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 })); } return; } - }; \ No newline at end of file + }; diff --git a/src/store/projects/project-create-actions.ts b/src/store/projects/project-create-actions.ts index 1fd1be0c..d226048b 100644 --- a/src/store/projects/project-create-actions.ts +++ b/src/store/projects/project-create-actions.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { reset, startSubmit, stopSubmit, initialize } from 'redux-form'; +import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form'; import { RootState } from '~/store/store'; import { dialogActions } from "~/store/dialog/dialog-actions"; import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service'; @@ -43,7 +43,7 @@ export const openProjectCreateDialog = (ownerUuid: string) => dispatch(initialize(PROJECT_CREATE_FORM_NAME, { userUuid })); } else { dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid })); - } + } dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_CREATE_FORM_NAME, data: {} })); }; @@ -58,7 +58,7 @@ export const createProject = (project: Partial) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' })); + dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors)); } return undefined; } diff --git a/src/store/projects/project-move-actions.ts b/src/store/projects/project-move-actions.ts index cacd49e6..365e07aa 100644 --- a/src/store/projects/project-move-actions.ts +++ b/src/store/projects/project-move-actions.ts @@ -4,7 +4,7 @@ import { Dispatch } from "redux"; import { dialogActions } from "~/store/dialog/dialog-actions"; -import { startSubmit, stopSubmit, initialize } from 'redux-form'; +import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form'; import { ServiceRepository } from '~/services/services'; import { RootState } from '~/store/store'; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; @@ -33,9 +33,9 @@ export const moveProject = (resource: MoveToFormDialogData) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' })); + dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' } as FormErrors)); } else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) { - dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' })); + dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' } as FormErrors)); } else { dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME })); throw new Error('Could not move the project.'); diff --git a/src/store/projects/project-update-actions.ts b/src/store/projects/project-update-actions.ts index 34ea42f5..321b8554 100644 --- a/src/store/projects/project-update-actions.ts +++ b/src/store/projects/project-update-actions.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { initialize, startSubmit, stopSubmit } from 'redux-form'; +import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; import { RootState } from "~/store/store"; import { dialogActions } from "~/store/dialog/dialog-actions"; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; @@ -38,7 +38,7 @@ export const updateProject = (project: Partial) => } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_VIOLATION) { - dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' })); + dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors)); } return ; } diff --git a/src/store/repositories/repositories-actions.ts b/src/store/repositories/repositories-actions.ts index 61caa769..ea64bfc9 100644 --- a/src/store/repositories/repositories-actions.ts +++ b/src/store/repositories/repositories-actions.ts @@ -10,7 +10,7 @@ import { navigateToRepositories } from "~/store/navigation/navigation-action"; import { unionize, ofType, UnionOf } from "~/common/unionize"; import { dialogActions } from '~/store/dialog/dialog-actions'; import { RepositoryResource } from "~/models/repositories"; -import { startSubmit, reset, stopSubmit } from "redux-form"; +import { startSubmit, reset, stopSubmit, FormErrors } from "redux-form"; import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service"; import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; @@ -55,13 +55,13 @@ export const createRepository = (repository: RepositoryResource) => const newRepository = await services.repositoriesService.create({ name: `${user.username}/${repository.name}` }); dispatch(dialogActions.CLOSE_DIALOG({ id: REPOSITORY_CREATE_FORM_NAME })); dispatch(reset(REPOSITORY_CREATE_FORM_NAME)); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Repository has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); - dispatch(loadRepositoriesData()); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Repository has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); + dispatch(loadRepositoriesData()); return newRepository; } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN) { - dispatch(stopSubmit(REPOSITORY_CREATE_FORM_NAME, { name: 'Repository with the same name already exists.' })); + dispatch(stopSubmit(REPOSITORY_CREATE_FORM_NAME, { name: 'Repository with the same name already exists.' } as FormErrors)); } return undefined; } @@ -104,4 +104,4 @@ export const loadRepositoriesData = () => export const loadRepositoriesPanel = () => (dispatch: Dispatch) => { dispatch(repositoriesBindedActions.REQUEST_ITEMS()); - }; \ No newline at end of file + }; diff --git a/src/store/search-bar/search-bar-actions.test.ts b/src/store/search-bar/search-bar-actions.test.ts new file mode 100644 index 00000000..aa6e4759 --- /dev/null +++ b/src/store/search-bar/search-bar-actions.test.ts @@ -0,0 +1,137 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { getAdvancedDataFromQuery, getQueryFromAdvancedData, parseSearchQuery } from "~/store/search-bar/search-bar-actions"; +import { ResourceKind } from "~/models/resource"; +import { ClusterObjectType } from "~/models/search-bar"; + +describe('search-bar-actions', () => { + describe('parseSearchQuery', () => { + it('should correctly parse query #1', () => { + const q = 'val0 is:trashed val1'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val1']); + expect(r.properties).toEqual({ + is: ['trashed'] + }); + }); + + it('should correctly parse query #2 (value with keyword should be ignored)', () => { + const q = 'val0 is:from:trashed val1'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val1']); + expect(r.properties).toEqual({ + from: ['trashed'] + }); + }); + + it('should correctly parse query #3 (many keywords)', () => { + const q = 'val0 is:trashed val2 from:2017-04-01 val1'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val2', 'val1']); + expect(r.properties).toEqual({ + is: ['trashed'], + from: ['2017-04-01'] + }); + }); + + it('should correctly parse query #4 (no duplicated values)', () => { + const q = 'val0 is:trashed val2 val2 val0'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val2']); + expect(r.properties).toEqual({ + is: ['trashed'] + }); + }); + + it('should correctly parse query #5 (properties)', () => { + const q = 'val0 has:filesize:100mb val2 val2 val0'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val2']); + expect(r.properties).toEqual({ + 'has': ['filesize:100mb'] + }); + }); + + it('should correctly parse query #6 (multiple properties, multiple is)', () => { + const q = 'val0 has:filesize:100mb val2 has:user:daniel is:starred val2 val0 is:trashed'; + const r = parseSearchQuery(q); + expect(r.hasKeywords).toBeTruthy(); + expect(r.values).toEqual(['val0', 'val2']); + expect(r.properties).toEqual({ + 'has': ['filesize:100mb', 'user:daniel'], + 'is': ['starred', 'trashed'] + }); + }); + }); + + describe('getAdvancedDataFromQuery', () => { + it('should correctly build advanced data record from query #1', () => { + const r = getAdvancedDataFromQuery('val0 has:filesize:100mb val2 has:user:daniel is:starred val2 val0 is:trashed'); + expect(r).toEqual({ + searchValue: 'val0 val2', + type: undefined, + cluster: undefined, + projectUuid: undefined, + inTrash: true, + dateFrom: undefined, + dateTo: undefined, + properties: [{ + key: 'filesize', + value: '100mb' + }, { + key: 'user', + value: 'daniel' + }], + saveQuery: false, + queryName: '' + }); + }); + + it('should correctly build advanced data record from query #2', () => { + const r = getAdvancedDataFromQuery('document from:2017-08-01 pdf has:filesize:101mb is:trashed type:arvados#collection cluster:indianapolis'); + expect(r).toEqual({ + searchValue: 'document pdf', + type: ResourceKind.COLLECTION, + cluster: ClusterObjectType.INDIANAPOLIS, + projectUuid: undefined, + inTrash: true, + dateFrom: '2017-08-01', + dateTo: undefined, + properties: [{ + key: 'filesize', + value: '101mb' + }], + saveQuery: false, + queryName: '' + }); + }); + }); + + describe('getQueryFromAdvancedData', () => { + it('should build query from advanced data', () => { + const q = getQueryFromAdvancedData({ + searchValue: 'document pdf', + type: ResourceKind.COLLECTION, + cluster: ClusterObjectType.INDIANAPOLIS, + projectUuid: undefined, + inTrash: true, + dateFrom: '2017-08-01', + dateTo: '', + properties: [{ + key: 'filesize', + value: '101mb' + }], + saveQuery: false, + queryName: '' + }); + expect(q).toBe('document pdf type:arvados#collection cluster:indianapolis is:trashed from:2017-08-01 has:filesize:101mb'); + }); + }); +}); diff --git a/src/store/search-bar/search-bar-actions.ts b/src/store/search-bar/search-bar-actions.ts index 165392c6..199ec3f9 100644 --- a/src/store/search-bar/search-bar-actions.ts +++ b/src/store/search-bar/search-bar-actions.ts @@ -2,22 +2,24 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { unionize, ofType, UnionOf } from "~/common/unionize"; +import { ofType, unionize, UnionOf } from "~/common/unionize"; import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service'; import { Dispatch } from 'redux'; -import { change, arrayPush } from 'redux-form'; +import { arrayPush, change, initialize } from 'redux-form'; import { RootState } from '~/store/store'; -import { initUserProject } from '~/store/tree-picker/tree-picker-actions'; +import { initUserProject, treePickerActions } from '~/store/tree-picker/tree-picker-actions'; import { ServiceRepository } from '~/services/services'; import { FilterBuilder } from "~/services/api/filter-builder"; import { ResourceKind } from '~/models/resource'; import { GroupClass } from '~/models/group'; import { SearchView } from '~/store/search-bar/search-bar-reducer'; -import { navigateToSearchResults, navigateTo } from '~/store/navigation/navigation-action'; +import { navigateTo, navigateToSearchResults } from '~/store/navigation/navigation-action'; import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; -import { initialize } from 'redux-form'; -import { SearchBarAdvanceFormData, PropertyValues } from '~/models/search-bar'; +import { ClusterObjectType, PropertyValue, SearchBarAdvanceFormData } from '~/models/search-bar'; import { debounce } from 'debounce'; +import * as _ from "lodash"; +import { getModifiedKeysValues } from "~/common/objects"; +import { activateSearchBarProject } from "~/store/search-bar/search-bar-tree-actions"; export const searchBarActions = unionize({ SET_CURRENT_VIEW: ofType(), @@ -61,7 +63,7 @@ export const searchData = (searchValue: string) => const currentView = getState().searchBar.currentView; dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue)); if (searchValue.length > 0) { - dispatch(searchGroups(searchValue, 5, {})); + dispatch(searchGroups(searchValue, 5)); if (currentView === SearchView.BASIC) { dispatch(searchBarActions.CLOSE_SEARCH_VIEW()); dispatch(navigateToSearchResults); @@ -77,11 +79,32 @@ export const searchAdvanceData = (data: SearchBarAdvanceFormData) => dispatch(navigateToSearchResults); }; +export const setSearchValueFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) => + (dispatch: Dispatch, getState: () => RootState) => { + const searchValue = getState().searchBar.searchValue; + const value = getQueryFromAdvancedData({ + ...data, + searchValue + }, prevData); + dispatch(searchBarActions.SET_SEARCH_VALUE(value)); + }; + +export const setAdvancedDataFromSearchValue = (search: string) => + async (dispatch: Dispatch) => { + const data = getAdvancedDataFromQuery(search); + dispatch(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data)); + if (data.projectUuid) { + await dispatch(activateSearchBarProject(data.projectUuid)); + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID, id: data.projectUuid })); + } + }; + const saveQuery = (data: SearchBarAdvanceFormData) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const savedQueries = services.searchService.getSavedQueries(); - if (data.saveQuery && data.searchQuery) { - const filteredQuery = savedQueries.find(query => query.searchQuery === data.searchQuery); + if (data.saveQuery && data.queryName) { + const filteredQuery = savedQueries.find(query => query.queryName === data.queryName); + data.searchValue = getState().searchBar.searchValue; if (filteredQuery) { services.searchService.editSavedQueries(data); dispatch(searchBarActions.UPDATE_SAVED_QUERY(savedQueries)); @@ -105,7 +128,7 @@ export const deleteSavedQuery = (id: number) => export const editSavedQuery = (data: SearchBarAdvanceFormData) => (dispatch: Dispatch) => { dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.ADVANCED)); - dispatch(searchBarActions.SET_SEARCH_VALUE(data.searchQuery)); + dispatch(searchBarActions.SET_SEARCH_VALUE(getQueryFromAdvancedData(data))); dispatch(initialize(SEARCH_BAR_ADVANCE_FORM_NAME, data)); }; @@ -127,6 +150,7 @@ export const closeSearchView = () => export const closeAdvanceView = () => (dispatch: Dispatch) => { dispatch(searchBarActions.SET_SEARCH_VALUE('')); + dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID })); dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.BASIC)); }; @@ -144,7 +168,7 @@ export const changeData = (searchValue: string) => const searchValuePresent = searchValue.length > 0; if (currentView === SearchView.ADVANCED) { - + dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.AUTOCOMPLETE)); } else if (searchValuePresent) { dispatch(searchBarActions.SET_CURRENT_VIEW(SearchView.AUTOCOMPLETE)); dispatch(searchBarActions.SET_SELECTED_ITEM(searchValue)); @@ -176,12 +200,12 @@ const startSearch = () => dispatch(searchData(searchValue)); }; -const searchGroups = (searchValue: string, limit: number, {...props}) => +const searchGroups = (searchValue: string, limit: number) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const currentView = getState().searchBar.currentView; if (searchValue || currentView === SearchView.ADVANCED) { - const filters = getFilters('name', searchValue, props); + const filters = getFilters('name', searchValue); const { items } = await services.groupsService.contents('', { filters, limit, @@ -191,16 +215,241 @@ const searchGroups = (searchValue: string, limit: number, {...props}) => } }; -export const getFilters = (filterName: string, searchValue: string, props: any): string => { - const { resourceKind, dateTo, dateFrom } = props; - return new FilterBuilder() - .addIsA("uuid", buildUuidFilter(resourceKind)) - .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION) - .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS) - .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT) - .addLte('modified_at', buildDateFilter(dateTo)) - .addGte('modified_at', buildDateFilter(dateFrom)) +const buildQueryFromKeyMap = (data: any, keyMap: string[][], mode: 'rebuild' | 'reuse') => { + let value = data.searchValue; + + const addRem = (field: string, key: string) => { + const v = data[key]; + + if (data.hasOwnProperty(key)) { + const pattern = v === false + ? `${field.replace(':', '\\:\\s*')}\\s*` + : `${field.replace(':', '\\:\\s*')}\\:\\s*[\\w|\\#|\\-|\\/]*\\s*`; + value = value.replace(new RegExp(pattern), ''); + } + + if (v) { + const nv = v === true + ? `${field}` + : `${field}:${v}`; + + if (mode === 'rebuild') { + value = value + ' ' + nv; + } else { + value = nv + ' ' + value; + } + } + }; + + keyMap.forEach(km => addRem(km[0], km[1])); + + return value; +}; + +export const getQueryFromAdvancedData = (data: SearchBarAdvanceFormData, prevData?: SearchBarAdvanceFormData) => { + let value = ''; + + const flatData = (data: SearchBarAdvanceFormData) => { + const fo = { + searchValue: data.searchValue, + type: data.type, + cluster: data.cluster, + projectUuid: data.projectUuid, + inTrash: data.inTrash, + dateFrom: data.dateFrom, + dateTo: data.dateTo, + }; + (data.properties || []).forEach(p => fo[`prop-${p.key}`] = p.value); + return fo; + }; + + const keyMap = [ + ['type', 'type'], + ['cluster', 'cluster'], + ['project', 'projectUuid'], + ['is:trashed', 'inTrash'], + ['from', 'dateFrom'], + ['to', 'dateTo'] + ]; + _.union(data.properties, prevData ? prevData.properties : []) + .forEach(p => keyMap.push([`has:${p.key}`, `prop-${p.key}`])); + + if (prevData) { + const obj = getModifiedKeysValues(flatData(data), flatData(prevData)); + value = buildQueryFromKeyMap({ + searchValue: data.searchValue, + ...obj + } as SearchBarAdvanceFormData, keyMap, "reuse"); + } else { + value = buildQueryFromKeyMap(flatData(data), keyMap, "rebuild"); + } + + value = value.trim(); + return value; +}; + +export interface ParseSearchQuery { + hasKeywords: boolean; + values: string[]; + properties: { + [key: string]: string[] + }; +} + +export const parseSearchQuery: (query: string) => ParseSearchQuery = (searchValue: string) => { + const keywords = [ + 'type:', + 'cluster:', + 'project:', + 'is:', + 'from:', + 'to:', + 'has:' + ]; + + const hasKeywords = (search: string) => keywords.reduce((acc, keyword) => acc + (search.includes(keyword) ? 1 : 0), 0); + let keywordsCnt = 0; + + const properties = {}; + + keywords.forEach(k => { + let p = searchValue.indexOf(k); + const key = k.substr(0, k.length - 1); + + while (p >= 0) { + const l = searchValue.length; + keywordsCnt += 1; + + let v = ''; + let i = p + k.length; + while (i < l && searchValue[i] === ' ') { + ++i; + } + const vp = i; + while (i < l && searchValue[i] !== ' ') { + v += searchValue[i]; + ++i; + } + + if (hasKeywords(v)) { + searchValue = searchValue.substr(0, p) + searchValue.substr(vp); + } else { + if (v !== '') { + if (!properties[key]) { + properties[key] = []; + } + properties[key].push(v); + } + searchValue = searchValue.substr(0, p) + searchValue.substr(i); + } + p = searchValue.indexOf(k); + } + }); + + const values = _.uniq(searchValue.split(' ').filter(v => v.length > 0)); + + return { hasKeywords: keywordsCnt > 0, values, properties }; +}; + +const getFirstProp = (sq: ParseSearchQuery, name: string) => sq.properties[name] && sq.properties[name][0]; +const getPropValue = (sq: ParseSearchQuery, name: string, value: string) => sq.properties[name] && sq.properties[name].find((v: string) => v === value); +const getProperties = (sq: ParseSearchQuery): PropertyValue[] => { + if (sq.properties.has) { + return sq.properties.has.map((value: string) => { + const v = value.split(':'); + return { + key: v[0], + value: v[1] + }; + }); + } + return []; +}; + +export const getAdvancedDataFromQuery = (query: string): SearchBarAdvanceFormData => { + const sq = parseSearchQuery(query); + + return { + searchValue: sq.values.join(' '), + type: getFirstProp(sq, 'type') as ResourceKind, + cluster: getFirstProp(sq, 'cluster') as ClusterObjectType, + projectUuid: getFirstProp(sq, 'project'), + inTrash: getPropValue(sq, 'is', 'trashed') !== undefined, + dateFrom: getFirstProp(sq, 'from'), + dateTo: getFirstProp(sq, 'to'), + properties: getProperties(sq), + saveQuery: false, + queryName: '' + }; +}; + +export const getFilters = (filterName: string, searchValue: string): string => { + const filter = new FilterBuilder(); + const sq = parseSearchQuery(searchValue); + + const resourceKind = getFirstProp(sq, 'type') as ResourceKind; + + let prefix = ''; + switch (resourceKind) { + case ResourceKind.COLLECTION: + prefix = GroupContentsResourcePrefix.COLLECTION; + break; + case ResourceKind.PROCESS: + prefix = GroupContentsResourcePrefix.PROCESS; + break; + default: + prefix = GroupContentsResourcePrefix.PROJECT; + break; + } + + if (!sq.hasKeywords) { + filter + .addILike(filterName, searchValue, GroupContentsResourcePrefix.COLLECTION) + .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROCESS) + .addILike(filterName, searchValue, GroupContentsResourcePrefix.PROJECT); + } else { + if (prefix) { + sq.values.forEach(v => + filter.addILike(filterName, v, prefix) + ); + } else { + sq.values.forEach(v => { + filter + .addILike(filterName, v, GroupContentsResourcePrefix.COLLECTION) + .addILike(filterName, v, GroupContentsResourcePrefix.PROCESS) + .addILike(filterName, v, GroupContentsResourcePrefix.PROJECT); + }); + } + + if (getPropValue(sq, 'is', 'trashed')) { + filter.addEqual("is_trashed", true); + } + + const projectUuid = getFirstProp(sq, 'project'); + if (projectUuid) { + filter.addEqual('uuid', projectUuid, prefix); + } + + const dateFrom = getFirstProp(sq, 'from'); + if (dateFrom) { + filter.addGte('modified_at', buildDateFilter(dateFrom)); + } + + const dateTo = getFirstProp(sq, 'to'); + if (dateTo) { + filter.addLte('modified_at', buildDateFilter(dateTo)); + } + + const props = getProperties(sq); + props.forEach(p => { + // filter.addILike(`properties.${p.key}`, p.value); + filter.addExists(p.key); + }); + } + + return filter .addEqual('groupClass', GroupClass.PROJECT, GroupContentsResourcePrefix.PROJECT) + .addIsA("uuid", buildUuidFilter(resourceKind)) .getFilters(); }; @@ -217,12 +466,12 @@ export const initAdvanceFormProjectsTree = () => dispatch(initUserProject(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)); }; -export const changeAdvanceFormProperty = (property: string, value: PropertyValues[] | string = '') => +export const changeAdvanceFormProperty = (property: string, value: PropertyValue[] | string = '') => (dispatch: Dispatch) => { dispatch(change(SEARCH_BAR_ADVANCE_FORM_NAME, property, value)); }; -export const updateAdvanceFormProperties = (propertyValues: PropertyValues) => +export const updateAdvanceFormProperties = (propertyValues: PropertyValue) => (dispatch: Dispatch) => { dispatch(arrayPush(SEARCH_BAR_ADVANCE_FORM_NAME, 'properties', propertyValues)); }; diff --git a/src/store/search-bar/search-bar-reducer.ts b/src/store/search-bar/search-bar-reducer.ts index 8508c05d..4f663eeb 100644 --- a/src/store/search-bar/search-bar-reducer.ts +++ b/src/store/search-bar/search-bar-reducer.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { searchBarActions, SearchBarActions } from '~/store/search-bar/search-bar-actions'; +import { + getQueryFromAdvancedData, + searchBarActions, + SearchBarActions +} from '~/store/search-bar/search-bar-actions'; import { GroupContentsResource } from '~/services/groups-service/groups-service'; import { SearchBarAdvanceFormData } from '~/models/search-bar'; @@ -45,7 +49,7 @@ const makeSelectedItem = (id: string, query?: string): SearchBarSelectedItem => 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)); + const savedIds = savedQueries.map((q, idx) => makeSelectedItem(`SQ-${idx}-${q.queryName}`, getQueryFromAdvancedData(q))); return recentIds.concat(savedIds); }; diff --git a/src/store/search-bar/search-bar-tree-actions.ts b/src/store/search-bar/search-bar-tree-actions.ts new file mode 100644 index 00000000..5101055a --- /dev/null +++ b/src/store/search-bar/search-bar-tree-actions.ts @@ -0,0 +1,101 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { getTreePicker, TreePicker } from "~/store/tree-picker/tree-picker"; +import { getNode, getNodeAncestorsIds, initTreeNode, TreeNodeStatus } from "~/models/tree"; +import { Dispatch } from "redux"; +import { RootState } from "~/store/store"; +import { ServiceRepository } from "~/services/services"; +import { treePickerActions } from "~/store/tree-picker/tree-picker-actions"; +import { FilterBuilder } from "~/services/api/filter-builder"; +import { OrderBuilder } from "~/services/api/order-builder"; +import { ProjectResource } from "~/models/project"; +import { resourcesActions } from "~/store/resources/resources-actions"; +import { SEARCH_BAR_ADVANCE_FORM_PICKER_ID } from "~/store/search-bar/search-bar-actions"; + +const getSearchBarTreeNode = (id: string) => (treePicker: TreePicker) => { + const searchTree = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(treePicker); + return searchTree + ? getNode(id)(searchTree) + : undefined; +}; + +export const loadSearchBarTreeProjects = (projectUuid: string) => + async (dispatch: Dispatch, getState: () => RootState) => { + const treePicker = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(getState().treePicker); + const node = treePicker ? getNode(projectUuid)(treePicker) : undefined; + if (node || projectUuid === '') { + await dispatch(loadSearchBarProject(projectUuid)); + } + }; + +export const getSearchBarTreeNodeAncestorsIds = (id: string) => (treePicker: TreePicker) => { + const searchTree = getTreePicker(SEARCH_BAR_ADVANCE_FORM_PICKER_ID)(treePicker); + return searchTree + ? getNodeAncestorsIds(id)(searchTree) + : []; +}; + +export const activateSearchBarTreeBranch = (id: string) => + async (dispatch: Dispatch, _: void, services: ServiceRepository) => { + const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || ''); + + for (const ancestor of ancestors) { + await dispatch(loadSearchBarTreeProjects(ancestor.uuid)); + } + dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ + ids: [ + ...[], + ...ancestors.map(ancestor => ancestor.uuid) + ], + pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID + })); + dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID })); + }; + +export const expandSearchBarTreeItem = (id: string) => + async (dispatch: Dispatch, getState: () => RootState) => { + const node = getSearchBarTreeNode(id)(getState().treePicker); + if (node && !node.expanded) { + dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID })); + } + }; + +export const activateSearchBarProject = (id: string) => + async (dispatch: Dispatch, getState: () => RootState) => { + const { treePicker } = getState(); + const node = getSearchBarTreeNode(id)(treePicker); + if (node && node.status !== TreeNodeStatus.LOADED) { + await dispatch(loadSearchBarTreeProjects(id)); + } else if (node === undefined) { + await dispatch(activateSearchBarTreeBranch(id)); + } + dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({ + ids: getSearchBarTreeNodeAncestorsIds(id)(treePicker), + pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID + })); + dispatch(expandSearchBarTreeItem(id)); + }; + + +const loadSearchBarProject = (projectUuid: string) => + async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => { + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: projectUuid, pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID })); + const params = { + filters: new FilterBuilder() + .addEqual('ownerUuid', projectUuid) + .getFilters(), + order: new OrderBuilder() + .addAsc('name') + .getOrder() + }; + const { items } = await services.projectService.list(params); + dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ + id: projectUuid, + pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID, + nodes: items.map(item => initTreeNode({ id: item.uuid, value: item })), + })); + dispatch(resourcesActions.SET_RESOURCES(items)); + }; + diff --git a/src/store/search-results-panel/search-results-middleware-service.ts b/src/store/search-results-panel/search-results-middleware-service.ts index b7607e31..1bd294f1 100644 --- a/src/store/search-results-panel/search-results-middleware-service.ts +++ b/src/store/search-results-panel/search-results-middleware-service.ts @@ -40,7 +40,7 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic export const getParams = (dataExplorer: DataExplorer, searchValue: string) => ({ ...dataExplorerToListParams(dataExplorer), - filters: getFilters('name', searchValue, {}), + filters: getFilters('name', searchValue), order: getOrder(dataExplorer) }); diff --git a/src/views-components/form-fields/search-bar-form-fields.tsx b/src/views-components/form-fields/search-bar-form-fields.tsx index 7a5703fe..da0b12b5 100644 --- a/src/views-components/form-fields/search-bar-form-fields.tsx +++ b/src/views-components/form-fields/search-bar-form-fields.tsx @@ -12,6 +12,8 @@ import { ClusterObjectType } from '~/models/search-bar'; import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker'; import { SEARCH_BAR_ADVANCE_FORM_PICKER_ID } from '~/store/search-bar/search-bar-actions'; import { SearchBarAdvancedPropertiesView } from '~/views-components/search-bar/search-bar-advanced-properties-view'; +import { TreeItem } from "~/components/tree/tree"; +import { ProjectsTreePickerItem } from "~/views-components/projects-tree-picker/generic-projects-tree-picker"; export const SearchBarTypeField = () => const ProjectsPicker = (props: WrappedFieldProps) =>
- + ) => { + props.input.onChange(id); + } + }/>
; export const SearchBarTrashField = () => @@ -82,10 +90,10 @@ export const SearchBarSaveSearchField = () => ; + label="Save query" />; export const SearchBarQuerySearchField = () => ; \ No newline at end of file + label="Query name" />; diff --git a/src/views-components/search-bar/search-bar-advanced-properties-view.tsx b/src/views-components/search-bar/search-bar-advanced-properties-view.tsx index 0384de22..d4044f95 100644 --- a/src/views-components/search-bar/search-bar-advanced-properties-view.tsx +++ b/src/views-components/search-bar/search-bar-advanced-properties-view.tsx @@ -13,10 +13,11 @@ import { changeAdvanceFormProperty, updateAdvanceFormProperties } from '~/store/search-bar/search-bar-actions'; -import { PropertyValues } from '~/models/search-bar'; +import { PropertyValue } from '~/models/search-bar'; import { ArvadosTheme } from '~/common/custom-theme'; import { SearchBarKeyField, SearchBarValueField } from '~/views-components/form-fields/search-bar-form-fields'; import { Chips } from '~/components/chips/chips'; +import { formatPropertyValue } from "~/common/formatters"; type CssRules = 'label' | 'button'; @@ -35,14 +36,14 @@ interface SearchBarAdvancedPropertiesViewDataProps { submitting: boolean; invalid: boolean; pristine: boolean; - propertyValues: PropertyValues; - fields: PropertyValues[]; + propertyValues: PropertyValue; + fields: PropertyValue[]; } interface SearchBarAdvancedPropertiesViewActionProps { setProps: () => void; - addProp: (propertyValues: PropertyValues) => void; - getAllFields: (propertyValues: PropertyValues[]) => PropertyValues[] | []; + addProp: (propertyValues: PropertyValue) => void; + getAllFields: (propertyValues: PropertyValue[]) => PropertyValue[] | []; } type SearchBarAdvancedPropertiesViewProps = SearchBarAdvancedPropertiesViewDataProps @@ -57,10 +58,10 @@ const mapStateToProps = (state: RootState) => { }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - setProps: (propertyValues: PropertyValues[]) => { + setProps: (propertyValues: PropertyValue[]) => { dispatch(changeAdvanceFormProperty('properties', propertyValues)); }, - addProp: (propertyValues: PropertyValues) => { + addProp: (propertyValues: PropertyValue) => { dispatch(updateAdvanceFormProperties(propertyValues)); dispatch(changeAdvanceFormProperty('key')); dispatch(changeAdvanceFormProperty('value')); @@ -95,8 +96,8 @@ export const SearchBarAdvancedPropertiesView = connect(mapStateToProps, mapDispa `${field.key}: ${field.value}`} /> + getLabel={(field: PropertyValue) => formatPropertyValue(field)} /> ) -); \ No newline at end of file +); diff --git a/src/views-components/search-bar/search-bar-advanced-view.tsx b/src/views-components/search-bar/search-bar-advanced-view.tsx index c7573501..05bdb970 100644 --- a/src/views-components/search-bar/search-bar-advanced-view.tsx +++ b/src/views-components/search-bar/search-bar-advanced-view.tsx @@ -6,7 +6,11 @@ import * as React from 'react'; import { reduxForm, InjectedFormProps, reset } from 'redux-form'; import { compose, Dispatch } from 'redux'; import { Paper, StyleRulesCallback, withStyles, WithStyles, Button, Grid, IconButton, CircularProgress } from '@material-ui/core'; -import { SEARCH_BAR_ADVANCE_FORM_NAME, searchAdvanceData } from '~/store/search-bar/search-bar-actions'; +import { + SEARCH_BAR_ADVANCE_FORM_NAME, SEARCH_BAR_ADVANCE_FORM_PICKER_ID, + searchAdvanceData, + setSearchValueFromAdvancedData +} from '~/store/search-bar/search-bar-actions'; import { ArvadosTheme } from '~/common/custom-theme'; import { CloseIcon } from '~/components/icon/icon'; import { SearchBarAdvanceFormData } from '~/models/search-bar'; @@ -15,6 +19,7 @@ import { SearchBarDateFromField, SearchBarDateToField, SearchBarPropertiesField, SearchBarSaveSearchField, SearchBarQuerySearchField } from '~/views-components/form-fields/search-bar-form-fields'; +import { treePickerActions } from "~/store/tree-picker/tree-picker-actions"; type CssRules = 'container' | 'closeIcon' | 'label' | 'buttonWrapper' | 'button' | 'circularProgress' | 'searchView' | 'selectGrid'; @@ -69,6 +74,7 @@ interface SearchBarAdvancedViewFormDataProps { // ToDo: maybe we should remove tags export interface SearchBarAdvancedViewDataProps { tags: any; + saveQuery: boolean; } export interface SearchBarAdvancedViewActionProps { @@ -99,10 +105,14 @@ export const SearchBarAdvancedView = compose( onSubmit: (data: SearchBarAdvanceFormData, dispatch: Dispatch) => { dispatch(searchAdvanceData(data)); dispatch(reset(SEARCH_BAR_ADVANCE_FORM_NAME)); - } + dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SEARCH_BAR_ADVANCE_FORM_PICKER_ID })); + }, + onChange: (data: SearchBarAdvanceFormData, dispatch: Dispatch, props: any, prevData: SearchBarAdvanceFormData) => { + dispatch(setSearchValueFromAdvancedData(data, prevData)); + }, }), withStyles(styles))( - ({ classes, closeAdvanceView, handleSubmit, submitting, invalid, pristine, tags }: SearchBarAdvancedViewFormProps) => + ({ classes, closeAdvanceView, handleSubmit, submitting, invalid, pristine, tags, saveQuery }: SearchBarAdvancedViewFormProps) =>
@@ -121,7 +131,7 @@ export const SearchBarAdvancedView = compose( Project - + @@ -152,7 +162,7 @@ export const SearchBarAdvancedView = compose( - + {saveQuery && } diff --git a/src/views-components/search-bar/search-bar-basic-view.tsx b/src/views-components/search-bar/search-bar-basic-view.tsx index 76d46b36..b23a96a0 100644 --- a/src/views-components/search-bar/search-bar-basic-view.tsx +++ b/src/views-components/search-bar/search-bar-basic-view.tsx @@ -32,10 +32,12 @@ const styles: StyleRulesCallback = theme => { color: theme.palette.primary.main }, label: { - fontSize: '0.875rem', + fontSize: '0.775rem', padding: `${theme.spacing.unit}px ${theme.spacing.unit}px `, color: theme.palette.grey["900"], - background: theme.palette.grey["200"] + background: 'white', + textAlign: 'right', + fontWeight: 'bold' } }; }; @@ -52,18 +54,17 @@ type SearchBarBasicViewProps = SearchBarBasicViewDataProps & SearchBarBasicViewA export const SearchBarBasicView = withStyles(styles)( ({ classes, onSetView, loadRecentQueries, deleteSavedQuery, savedQueries, onSearch, editSavedQuery, selectedItem }: SearchBarBasicViewProps) => -
Recent search queries
+
{"Recent queries"}
-
Saved search queries
+
{"Saved queries"}
-
onSetView(SearchView.ADVANCED)}>Advanced search
); diff --git a/src/views-components/search-bar/search-bar-save-queries.tsx b/src/views-components/search-bar/search-bar-save-queries.tsx index aa62c58f..5234c214 100644 --- a/src/views-components/search-bar/search-bar-save-queries.tsx +++ b/src/views-components/search-bar/search-bar-save-queries.tsx @@ -8,6 +8,7 @@ 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"; +import { getQueryFromAdvancedData } from "~/store/search-bar/search-bar-actions"; type CssRules = 'root' | 'listItem' | 'listItemText' | 'button'; @@ -48,10 +49,10 @@ export const SearchBarSavedQueries = withStyles(styles)( ({ classes, savedQueries, onSearch, editSavedQuery, deleteSavedQuery, selectedItem }: SearchBarSavedQueriesProps) => {savedQueries.map((query, index) => - + onSearch(query.searchQuery)} + secondary={query.queryName} + onClick={() => onSearch(getQueryFromAdvancedData(query))} className={classes.listItemText} /> diff --git a/src/views-components/search-bar/search-bar-view.tsx b/src/views-components/search-bar/search-bar-view.tsx index 1a19b47d..51ea3fa1 100644 --- a/src/views-components/search-bar/search-bar-view.tsx +++ b/src/views-components/search-bar/search-bar-view.tsx @@ -14,6 +14,7 @@ import { ClickAwayListener } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import { ArvadosTheme } from '~/common/custom-theme'; import { SearchView } from '~/store/search-bar/search-bar-reducer'; import { @@ -49,7 +50,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => { }, input: { border: 'none', - padding: `0px ${theme.spacing.unit}px` + padding: `0` }, view: { position: 'absolute', @@ -85,6 +86,7 @@ interface SearchBarViewActionProps { loadRecentQueries: () => string[]; moveUp: () => void; moveDown: () => void; + setAdvancedDataFromSearchValue: (search: string) => void; } type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles; @@ -93,6 +95,7 @@ const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => { if (e.keyCode === KEY_CODE_DOWN) { e.preventDefault(); if (!props.isPopoverOpen) { + props.onSetView(SearchView.AUTOCOMPLETE); props.openSearchView(); } else { props.moveDown(); @@ -116,6 +119,30 @@ const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => { } }; +const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => { + if (props.searchValue) { + props.onSetView(SearchView.AUTOCOMPLETE); + props.openSearchView(); + } else { + props.closeView(); + } +}; + +const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => { + e.stopPropagation(); + if (props.isPopoverOpen) { + if (props.currentView === SearchView.ADVANCED) { + props.closeView(); + } else { + props.setAdvancedDataFromSearchValue(props.searchValue); + props.onSetView(SearchView.ADVANCED); + } + } else { + props.setAdvancedDataFromSearchValue(props.searchValue); + props.onSetView(SearchView.ADVANCED); + } +}; + export const SearchBarView = withStyles(styles)( (props : SearchBarViewProps) => { const { classes, isPopoverOpen } = props; @@ -130,16 +157,25 @@ export const SearchBarView = withStyles(styles)( value={props.searchValue} fullWidth={true} disableUnderline={true} - onClick={props.openSearchView} + onClick={e => handleInputClick(e, props)} onKeyDown={e => handleKeyDown(e, props)} - endAdornment={ - + startAdornment={ + + } + endAdornment={ + + + handleDropdownClick(e, props)}> + + + + } />
@@ -162,7 +198,8 @@ const getView = (props: SearchBarViewProps) => { case SearchView.ADVANCED: return ; + tags={props.tags} + saveQuery={props.saveQuery} />; default: return searchResults: searchBar.searchResults, selectedItem: searchBar.selectedItem, savedQueries: searchBar.savedQueries, - tags: form.searchBarAdvanceFormName + tags: form.searchBarAdvanceFormName, + saveQuery: form.searchBarAdvanceFormName && + form.searchBarAdvanceFormName.values && + form.searchBarAdvanceFormName.values.saveQuery }; }; @@ -46,7 +49,8 @@ const mapDispatchToProps = (dispatch: Dispatch): SearchBarActionProps => ({ navigateTo: (uuid: string) => dispatch(navigateToItem(uuid)), editSavedQuery: (data: SearchBarAdvanceFormData) => dispatch(editSavedQuery(data)), moveUp: () => dispatch(moveUp()), - moveDown: () => dispatch(moveDown()) + moveDown: () => dispatch(moveDown()), + setAdvancedDataFromSearchValue: (search: string) => dispatch(setAdvancedDataFromSearchValue(search)) }); export const SearchBar = connect(mapStateToProps, mapDispatchToProps)(SearchBarView); diff --git a/src/views-components/sharing-dialog/sharing-management-form-component.tsx b/src/views-components/sharing-dialog/sharing-management-form-component.tsx index ad8f65fb..5e374042 100644 --- a/src/views-components/sharing-dialog/sharing-management-form-component.tsx +++ b/src/views-components/sharing-dialog/sharing-management-form-component.tsx @@ -4,7 +4,13 @@ import * as React from 'react'; import { Grid, StyleRulesCallback, Divider, IconButton, Typography } from '@material-ui/core'; -import { Field, WrappedFieldProps, WrappedFieldArrayProps, FieldArray, FieldsProps } from 'redux-form'; +import { + Field, + WrappedFieldProps, + WrappedFieldArrayProps, + FieldArray, + FieldArrayFieldsProps +} from 'redux-form'; import { PermissionSelect, formatPermissionLevel, parsePermissionLevel } from './permission-select'; import { WithStyles } from '@material-ui/core/styles'; import withStyles from '@material-ui/core/styles/withStyles'; @@ -29,7 +35,7 @@ const permissionManagementRowStyles: StyleRulesCallback<'root'> = theme => ({ } }); const PermissionManagementRow = withStyles(permissionManagementRowStyles)( - ({ field, index, fields, classes }: { field: string, index: number, fields: FieldsProps<{ email: string }> } & WithStyles<'root'>) => + ({ field, index, fields, classes }: { field: string, index: number, fields: FieldArrayFieldsProps<{ email: string }> } & WithStyles<'root'>) => <> diff --git a/yarn.lock b/yarn.lock index f08c370f..d3d6396d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -252,10 +252,10 @@ "@types/react" "*" redux "^3.6.0" -"@types/redux-form@7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.5.tgz#fae0fa6cfbc613867093d1e0f6a84db17177305e" - integrity sha512-PY74tuDamNhStB+87TQJXAKoa7uf5Ue/MJvnIrQowgjyRUo2Ky/THUfDec9U7IKRGzLnX7vWVTsoN1EvLnwAEQ== +"@types/redux-form@7.4.12": + version "7.4.12" + resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.12.tgz#2afb0615e3b7417d460ab14a4802ede4a98f9c79" + integrity sha512-qHRkJcgdc5MntQHrkYCg5o6oySh+OdVKA90yELTdi9XlNvSGRxd6K230aTckVrwdUjdxtwZ31UqFgLoU5SiWYQ== dependencies: "@types/react" "*" redux "^3.6.0 || ^4.0.0"