From dd0e60751f2551d78344283105e8c91adb9c3e19 Mon Sep 17 00:00:00 2001 From: Lucas Di Pentima Date: Fri, 31 Jan 2020 14:05:50 -0300 Subject: [PATCH] 15781: Adds multi-value property support for projects. Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- src/lib/resource-properties.ts | 35 +++++++++++++++++++ .../collection-panel-action.ts | 26 +++----------- .../details-panel/details-panel-action.ts | 13 +++---- .../details-panel/project-details.tsx | 9 ++--- .../project-properties-dialog.tsx | 21 +++++++---- .../property-chip.tsx | 6 ++++ .../collection-panel/collection-panel.tsx | 19 +++++----- 7 files changed, 80 insertions(+), 49 deletions(-) create mode 100644 src/lib/resource-properties.ts diff --git a/src/lib/resource-properties.ts b/src/lib/resource-properties.ts new file mode 100644 index 00000000..02f13b62 --- /dev/null +++ b/src/lib/resource-properties.ts @@ -0,0 +1,35 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +export const deleteProperty = (properties: any, key: string, value: string) => { + if (Array.isArray(properties[key])) { + properties[key] = properties[key].filter((v: string) => v !== value); + if (properties[key].length === 1) { + properties[key] = properties[key][0]; + } else if (properties[key].length === 0) { + delete properties[key]; + } + } else if (properties[key] === value) { + delete properties[key]; + } + return properties; +} + +export const addProperty = (properties: any, key: string, value: string) => { + if (properties[key]) { + if (Array.isArray(properties[key])) { + properties[key] = [...properties[key], value]; + } else { + properties[key] = [properties[key], value]; + } + // Remove potential duplicate and save as single value if needed + properties[key] = Array.from(new Set(properties[key])); + if (properties[key].length === 1) { + properties[key] = properties[key][0]; + } + } else { + properties[key] = value; + } + return properties; +} \ No newline at end of file diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts index f4244389..fee5bcd6 100644 --- a/src/store/collection-panel/collection-panel-action.ts +++ b/src/store/collection-panel/collection-panel-action.ts @@ -16,6 +16,7 @@ import { unionize, ofType, UnionOf } from '~/common/unionize'; import { SnackbarKind } from '~/store/snackbar/snackbar-actions'; import { navigateTo } from '~/store/navigation/navigation-action'; import { loadDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { deleteProperty, addProperty } from "~/lib/resource-properties"; export const collectionPanelActions = unionize({ SET_COLLECTION: ofType(), @@ -47,20 +48,10 @@ export const createCollectionTag = (data: TagProperty) => if (item) { const key = data.keyID || data.key; const value = data.valueID || data.value; - if (item.properties[key]) { - if (Array.isArray(item.properties[key])) { - item.properties[key] = [...item.properties[key], value]; - // Remove potential duplicates - item.properties[key] = Array.from(new Set(item.properties[key])); - } else { - item.properties[key] = [item.properties[key], value]; - } - } else { - item.properties[key] = value; - } + item.properties = addProperty(item.properties, key, value); const updatedCollection = await services.collectionService.update( uuid, { - properties: {...JSON.parse(JSON.stringify(item.properties))} + properties: {...item.properties} } ); item.properties = updatedCollection.properties; @@ -91,16 +82,7 @@ export const deleteCollectionTag = (key: string, value: string) => const uuid = item ? item.uuid : ''; try { if (item) { - if (Array.isArray(item.properties[key])) { - item.properties[key] = item.properties[key].filter((v: string) => v !== value); - if (item.properties[key].length === 1) { - item.properties[key] = item.properties[key][0]; - } else if (item.properties[key].length === 0) { - delete item.properties[key]; - } - } else if (item.properties[key] === value) { - delete item.properties[key]; - } + item.properties = deleteProperty(item.properties, key, value); const updatedCollection = await services.collectionService.update( uuid, { diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts index 68746714..e0d72017 100644 --- a/src/store/details-panel/details-panel-action.ts +++ b/src/store/details-panel/details-panel-action.ts @@ -13,6 +13,7 @@ import { TagProperty } from '~/models/tag'; import { startSubmit, stopSubmit } from 'redux-form'; import { resourcesActions } from '~/store/resources/resources-actions'; import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions'; +import { addProperty, deleteProperty } from '~/lib/resource-properties'; export const SLIDE_TIMEOUT = 500; @@ -36,13 +37,13 @@ export const openProjectPropertiesDialog = () => dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } })); }; -export const deleteProjectProperty = (key: string) => +export const deleteProjectProperty = (key: string, value: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const { detailsPanel, resources } = getState(); const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource; try { if (project) { - delete project.properties[key]; + project.properties = deleteProperty(project.properties, key, value); const updatedProject = await services.projectService.update(project.uuid, { properties: project.properties }); dispatch(resourcesActions.SET_RESOURCES([updatedProject])); dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); @@ -60,12 +61,12 @@ export const createProjectProperty = (data: TagProperty) => dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME)); try { if (project) { + const key = data.keyID || data.key; + const value = data.valueID || data.value; + project.properties = addProperty(project.properties, key, value); const updatedProject = await services.projectService.update( project.uuid, { - properties: { - ...JSON.parse(JSON.stringify(project.properties)), - [data.keyID || data.key]: data.valueID || data.value - } + properties: {...project.properties} } ); dispatch(resourcesActions.SET_RESOURCES([updatedProject])); diff --git a/src/views-components/details-panel/project-details.tsx b/src/views-components/details-panel/project-details.tsx index 59035da1..1be04b00 100644 --- a/src/views-components/details-panel/project-details.tsx +++ b/src/views-components/details-panel/project-details.tsx @@ -16,7 +16,7 @@ import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core'; import { ArvadosTheme } from '~/common/custom-theme'; import { Dispatch } from 'redux'; -import { PropertyChipComponent } from '../resource-properties-form/property-chip'; +import { getPropertyChip } from '../resource-properties-form/property-chip'; export class ProjectDetails extends DetailsData { getIcon(className?: string) { @@ -83,9 +83,10 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)( { Object.keys(project.properties).map(k => - + Array.isArray(project.properties[k]) + ? project.properties[k].map((v: string) => + getPropertyChip(k, v, undefined, classes.tag)) + : getPropertyChip(k, project.properties[k], undefined, classes.tag) ) } diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx index 7a4cfba6..e1874d95 100644 --- a/src/views-components/project-properties-dialog/project-properties-dialog.tsx +++ b/src/views-components/project-properties-dialog/project-properties-dialog.tsx @@ -13,7 +13,7 @@ import { Dialog, DialogTitle, DialogContent, DialogActions, Button, withStyles, import { ArvadosTheme } from '~/common/custom-theme'; import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form'; import { getResource } from '~/store/resources/resources'; -import { PropertyChipComponent } from "../resource-properties-form/property-chip"; +import { getPropertyChip } from "../resource-properties-form/property-chip"; type CssRules = 'tag'; @@ -29,7 +29,7 @@ interface ProjectPropertiesDialogDataProps { } interface ProjectPropertiesDialogActionProps { - handleDelete: (key: string) => void; + handleDelete: (key: string, value: string) => void; } const mapStateToProps = ({ detailsPanel, resources, properties }: RootState): ProjectPropertiesDialogDataProps => ({ @@ -37,7 +37,7 @@ const mapStateToProps = ({ detailsPanel, resources, properties }: RootState): Pr }); const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({ - handleDelete: (key: string) => dispatch(deleteProjectProperty(key)), + handleDelete: (key: string, value: string) => () => dispatch(deleteProjectProperty(key, value)), }); type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles; @@ -55,10 +55,17 @@ export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToPro {project && project.properties && Object.keys(project.properties).map(k => - handleDelete(k)} - key={k} className={classes.tag} - propKey={k} propValue={project.properties[k]} />) + Array.isArray(project.properties[k]) + ? project.properties[k].map((v: string) => + getPropertyChip( + k, v, + handleDelete(k, v), + classes.tag)) + : getPropertyChip( + k, project.properties[k], + handleDelete(k, project.properties[k]), + classes.tag) + ) } diff --git a/src/views-components/resource-properties-form/property-chip.tsx b/src/views-components/resource-properties-form/property-chip.tsx index c51a8d8e..f25deb70 100644 --- a/src/views-components/resource-properties-form/property-chip.tsx +++ b/src/views-components/resource-properties-form/property-chip.tsx @@ -50,3 +50,9 @@ export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps ); } ); + +export const getPropertyChip = (k:string, v:string, handleDelete:any, className:string) => + ; diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index cc918808..c4221937 100644 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@ -23,7 +23,7 @@ import { ContextMenuKind } from '~/views-components/context-menu/context-menu'; import { formatFileSize } from "~/common/formatters"; import { openDetailsPanel } from '~/store/details-panel/details-panel-action'; import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions'; -import { PropertyChipComponent } from '~/views-components/resource-properties-form/property-chip'; +import { getPropertyChip } from '~/views-components/resource-properties-form/property-chip'; import { IllegalNamingWarning } from '~/components/warning/warning'; type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link'; @@ -130,8 +130,14 @@ export const CollectionPanel = withStyles(styles)( {Object.keys(item.properties).map(k => Array.isArray(item.properties[k]) ? item.properties[k].map((v: string) => - getPropertyChip(k, v, this.handleDelete, classes.tag)) - : getPropertyChip(k, item.properties[k], this.handleDelete, classes.tag) + getPropertyChip( + k, v, + this.handleDelete(k, v), + classes.tag)) + : getPropertyChip( + k, item.properties[k], + this.handleDelete(k, item.properties[k]), + classes.tag) )} @@ -184,10 +190,3 @@ export const CollectionPanel = withStyles(styles)( } ) ); - -const getPropertyChip = (k:string, v:string, handleDelete:any, className:string) => - ; - -- 2.30.2