From 82908c571a492f19f2ea402e033fa84b6df15b61 Mon Sep 17 00:00:00 2001 From: Janicki Artur Date: Fri, 16 Nov 2018 13:38:35 +0100 Subject: [PATCH] Add properties inside projects and create modal to manage. Feature #14433_properties_inside_projects Arvados-DCO-1.1-Signed-off-by: Janicki Artur --- .../details-panel/details-panel-action.ts | 54 ++++++++++- .../details-panel/project-details.tsx | 89 +++++++++++++---- .../project-properties-dialog.tsx | 73 ++++++++++++++ .../project-properties-form.tsx | 95 +++++++++++++++++++ src/views/workbench/workbench.tsx | 2 + 5 files changed, 291 insertions(+), 22 deletions(-) create mode 100644 src/views-components/project-properties-dialog/project-properties-dialog.tsx create mode 100644 src/views-components/project-properties-dialog/project-properties-form.tsx diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts index 2724a3e3..cd9ab4b1 100644 --- a/src/store/details-panel/details-panel-action.ts +++ b/src/store/details-panel/details-panel-action.ts @@ -3,6 +3,16 @@ // SPDX-License-Identifier: AGPL-3.0 import { unionize, ofType, UnionOf } from '~/common/unionize'; +import { RootState } from '~/store/store'; +import { Dispatch } from 'redux'; +import { dialogActions } from '~/store/dialog/dialog-actions'; +import { getResource } from '~/store/resources/resources'; +import { ProjectResource } from "~/models/project"; +import { ServiceRepository } from '~/services/services'; +import { TagProperty } from '~/models/tag'; +import { startSubmit, stopSubmit } from 'redux-form'; +import { resourcesActions } from '~/store/resources/resources-actions'; +import { snackbarActions } from '~/store/snackbar/snackbar-actions'; export const detailsPanelActions = unionize({ TOGGLE_DETAILS_PANEL: ofType<{}>(), @@ -11,8 +21,50 @@ export const detailsPanelActions = unionize({ export type DetailsPanelAction = UnionOf; -export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid); +export const PROJECT_PROPERTIES_FORM_NAME = 'projectPropertiesFormName'; +export const PROJECT_PROPERTIES_DIALOG_NAME = 'projectPropertiesDialogName'; +export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid); +export const openProjectPropertiesDialog = () => + (dispatch: Dispatch) => { + dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } })); + }; +export const deleteProjectProperty = (key: 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]; + const updatedProject = await services.projectService.update(project.uuid, project); + dispatch(resourcesActions.SET_RESOURCES([updatedProject])); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000 })); + } + return; + } catch (e) { + dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME })); + throw new Error('Could not remove property from the project.'); + } + }; +export const createProjectProperty = (data: TagProperty) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { detailsPanel, resources } = getState(); + const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource; + dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME)); + try { + if (project) { + project.properties[data.key] = data.value; + const updatedProject = await services.projectService.update(project.uuid, project); + dispatch(resourcesActions.SET_RESOURCES([updatedProject])); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000 })); + dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME)); + } + return; + } catch (e) { + dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME })); + throw new Error('Could not add property to the project.'); + } + }; \ No newline at end of file diff --git a/src/views-components/details-panel/project-details.tsx b/src/views-components/details-panel/project-details.tsx index 18affbac..e9952910 100644 --- a/src/views-components/details-panel/project-details.tsx +++ b/src/views-components/details-panel/project-details.tsx @@ -3,7 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { ProjectIcon } from '~/components/icon/icon'; +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action'; +import { ProjectIcon, RenameIcon } from '~/components/icon/icon'; import { ProjectResource } from '~/models/project'; import { formatDate } from '~/common/formatters'; import { ResourceKind } from '~/models/resource'; @@ -11,32 +14,76 @@ import { resourceLabel } from '~/common/labels'; import { DetailsData } from "./details-data"; import { DetailsAttribute } from "~/components/details-attribute/details-attribute"; import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text-editor-link'; +import { withStyles, StyleRulesCallback, Chip, WithStyles } from '@material-ui/core'; +import { ArvadosTheme } from '~/common/custom-theme'; export class ProjectDetails extends DetailsData { - getIcon(className?: string) { return ; } getDetails() { - return
- - {/* Missing attr */} - - - - - {/* Missing attr */} - {/**/} - - {this.item.description ? - - : '---' - } - -
; + return ; + } +} + +type CssRules = 'tag' | 'editIcon'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + tag: { + marginRight: theme.spacing.unit, + marginBottom: theme.spacing.unit + }, + editIcon: { + fontSize: '1.125rem', + cursor: 'pointer' } +}); + + +interface ProjectDetailsComponentDataProps { + project: ProjectResource; +} + +interface ProjectDetailsComponentActionProps { + onClick: () => void; } + +const mapDispatchToProps = (dispatch: Dispatch): ProjectDetailsComponentActionProps => ({ + onClick: () => dispatch(openProjectPropertiesDialog()) +}); + +type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles; + +const ProjectDetailsComponent = connect(null, mapDispatchToProps)( + withStyles(styles)( + ({ classes, project, onClick }: ProjectDetailsComponentProps) =>
+ + {/* Missing attr */} + + + + + {/* Missing attr */} + {/**/} + + {project.description ? + + : '---' + } + + +
+ +
+
+ { + Object.keys(project.properties).map(k => { + return ; + }) + } +
+)); \ No newline at end of file diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx new file mode 100644 index 00000000..d165f981 --- /dev/null +++ b/src/views-components/project-properties-dialog/project-properties-dialog.tsx @@ -0,0 +1,73 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { Dispatch } from "redux"; +import { connect } from "react-redux"; +import { RootState } from '~/store/store'; +import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog"; +import { ProjectResource } from '~/models/project'; +import { PROJECT_PROPERTIES_DIALOG_NAME, deleteProjectProperty } from '~/store/details-panel/details-panel-action'; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form'; +import { getResource } from '~/store/resources/resources'; + +type CssRules = 'tag'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + tag: { + marginRight: theme.spacing.unit, + marginBottom: theme.spacing.unit + } +}); + +interface ProjectPropertiesDialogDataProps { + project: ProjectResource; +} + +interface ProjectPropertiesDialogActionProps { + handleDelete: (key: string) => void; +} + +const mapStateToProps = ({ detailsPanel, resources }: RootState): ProjectPropertiesDialogDataProps => { + const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource; + return { project }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({ + handleDelete: (key: string) => dispatch(deleteProjectProperty(key)) +}); + +type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles; + +export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)( + withStyles(styles)( + withDialog(PROJECT_PROPERTIES_DIALOG_NAME)( + ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) => + + Properties + + + {project && project.properties && + Object.keys(project.properties).map(k => { + return handleDelete(k)} + label={`${k}: ${project.properties[k]}`} />; + }) + } + + + + + +))); \ No newline at end of file diff --git a/src/views-components/project-properties-dialog/project-properties-form.tsx b/src/views-components/project-properties-dialog/project-properties-form.tsx new file mode 100644 index 00000000..82ae0406 --- /dev/null +++ b/src/views-components/project-properties-dialog/project-properties-form.tsx @@ -0,0 +1,95 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { reduxForm, Field, reset } from 'redux-form'; +import { compose, Dispatch } from 'redux'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core'; +import { TagProperty } from '~/models/tag'; +import { TextField } from '~/components/text-field/text-field'; +import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators'; +import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from '~/store/details-panel/details-panel-action'; + +type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + display: 'flex' + }, + keyField: { + width: '40%', + marginRight: theme.spacing.unit * 3 + }, + valueField: { + width: '40%', + marginRight: theme.spacing.unit * 3 + }, + buttonWrapper: { + paddingTop: '14px', + position: 'relative', + }, + saveButton: { + boxShadow: 'none' + }, + circularProgress: { + position: 'absolute', + top: -9, + bottom: 0, + left: 0, + right: 0, + margin: 'auto' + } +}); + +interface ProjectPropertiesFormDataProps { + submitting: boolean; + invalid: boolean; + pristine: boolean; +} + +interface ProjectPropertiesFormActionProps { + handleSubmit: any; +} + +type ProjectPropertiesFormProps = ProjectPropertiesFormDataProps & ProjectPropertiesFormActionProps & WithStyles; + +export const ProjectPropertiesForm = compose( + reduxForm({ + form: PROJECT_PROPERTIES_FORM_NAME, + onSubmit: (data: TagProperty, dispatch: Dispatch) => { + dispatch(createProjectProperty(data)); + dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); + } + }), + withStyles(styles))( + ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) => +
+
+ +
+
+ +
+
+ + {submitting && } +
+
+ ); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 92b2b5b7..19a2ef49 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -46,6 +46,7 @@ import { SearchResultsPanel } from '~/views/search-results-panel/search-results- import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog'; import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog'; import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog'; +import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -140,6 +141,7 @@ export const WorkbenchPanel = + -- 2.30.2