[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