[ARVADOS-WORKBENCH2] created: 1.2.0-866-g82908c5
Git user
git at public.curoverse.com
Fri Nov 16 07:38:51 EST 2018
at 82908c571a492f19f2ea402e033fa84b6df15b61 (commit)
commit 82908c571a492f19f2ea402e033fa84b6df15b61
Author: Janicki Artur <artur.janicki at contractors.roche.com>
Date: Fri Nov 16 13:38:35 2018 +0100
Add properties inside projects and create modal to manage.
Feature #14433_properties_inside_projects
Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki at contractors.roche.com>
diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts
index 2724a3e..cd9ab4b 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<typeof detailsPanelActions>;
-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<any>(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 18affba..e995291 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<ProjectResource> {
-
getIcon(className?: string) {
return <ProjectIcon className={className} />;
}
getDetails() {
- return <div>
- <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
- {/* Missing attr */}
- <DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
- <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
- <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
- {/* Missing attr */}
- {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
- <DetailsAttribute label='Description'>
- {this.item.description ?
- <RichTextEditorLink
- title={`Description of ${this.item.name}`}
- content={this.item.description}
- label='Show full description' />
- : '---'
- }
- </DetailsAttribute>
- </div>;
+ return <ProjectDetailsComponent project={this.item} />;
+ }
+}
+
+type CssRules = 'tag' | 'editIcon';
+
+const styles: StyleRulesCallback<CssRules> = (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<any>(openProjectPropertiesDialog())
+});
+
+type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
+
+const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
+ withStyles(styles)(
+ ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
+ <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
+ {/* Missing attr */}
+ <DetailsAttribute label='Size' value='---' />
+ <DetailsAttribute label='Owner' value={project.ownerUuid} lowercaseValue={true} />
+ <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
+ <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
+ {/* Missing attr */}
+ {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
+ <DetailsAttribute label='Description'>
+ {project.description ?
+ <RichTextEditorLink
+ title={`Description of ${project.name}`}
+ content={project.description}
+ label='Show full description' />
+ : '---'
+ }
+ </DetailsAttribute>
+ <DetailsAttribute label='Properties'>
+ <div onClick={onClick}>
+ <RenameIcon className={classes.editIcon} />
+ </div>
+ </DetailsAttribute>
+ {
+ Object.keys(project.properties).map(k => {
+ return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
+ })
+ }
+ </div>
+));
\ 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 0000000..d165f98
--- /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<CssRules> = (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<any>(deleteProjectProperty(key))
+});
+
+type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
+
+export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
+ withStyles(styles)(
+ withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
+ ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
+ <Dialog open={open}
+ onClose={closeDialog}
+ fullWidth
+ maxWidth='sm'>
+ <DialogTitle>Properties</DialogTitle>
+ <DialogContent>
+ <ProjectPropertiesForm />
+ {project && project.properties &&
+ Object.keys(project.properties).map(k => {
+ return <Chip key={k} className={classes.tag}
+ onDelete={() => handleDelete(k)}
+ label={`${k}: ${project.properties[k]}`} />;
+ })
+ }
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+)));
\ 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 0000000..82ae040
--- /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<CssRules> = (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<CssRules>;
+
+export const ProjectPropertiesForm = compose(
+ reduxForm({
+ form: PROJECT_PROPERTIES_FORM_NAME,
+ onSubmit: (data: TagProperty, dispatch: Dispatch) => {
+ dispatch<any>(createProjectProperty(data));
+ dispatch(reset(PROJECT_PROPERTIES_FORM_NAME));
+ }
+ }),
+ withStyles(styles))(
+ ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) =>
+ <form onSubmit={handleSubmit} className={classes.root}>
+ <div className={classes.keyField}>
+ <Field name="key"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_KEY_VALIDATION}
+ label="Key" />
+ </div>
+ <div className={classes.valueField}>
+ <Field name="value"
+ disabled={submitting}
+ component={TextField}
+ validate={TAG_VALUE_VALIDATION}
+ label="Value" />
+ </div>
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ size='small'
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ ADD
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </form>
+ );
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 92b2b5b..19a2ef4 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 =
<PartialCopyCollectionDialog />
<ProcessCommandDialog />
<ProcessInputDialog />
+ <ProjectPropertiesDialog />
<RenameFileDialog />
<RichTextEditorDialog />
<SharingDialog />
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list