18219: Adds property edition capabilities to create & update dialogs.
authorLucas Di Pentima <lucas.dipentima@curii.com>
Fri, 17 Dec 2021 21:03:14 +0000 (18:03 -0300)
committerLucas Di Pentima <lucas.dipentima@curii.com>
Fri, 17 Dec 2021 22:39:01 +0000 (19:39 -0300)
There's too much code duplication. Some might be simple to avoid.

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima@curii.com>

17 files changed:
src/store/collections/collection-create-actions.ts
src/store/collections/collection-update-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/projects/project-update-actions.ts
src/views-components/collection-properties/create-collection-properties-form.tsx [new file with mode: 0644]
src/views-components/collection-properties/create-collection-properties-list.tsx [new file with mode: 0644]
src/views-components/collection-properties/update-collection-properties-form.tsx [new file with mode: 0644]
src/views-components/collection-properties/update-collection-properties-list.tsx [new file with mode: 0644]
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/dialog-forms/create-collection-dialog.ts
src/views-components/dialog-update/dialog-collection-update.tsx
src/views-components/dialog-update/dialog-project-update.tsx
src/views-components/project-properties/update-project-properties-form.tsx [new file with mode: 0644]
src/views-components/project-properties/update-project-properties-list.tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/project-panel/project-panel.tsx

index 81d8948ce1dfa65ffb84b7362c0b940124272ba4..22202b15985427fc04dfa52554039ee532fcb6c2 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
+import { reset, startSubmit, stopSubmit, initialize, FormErrors, change, formValueSelector } from 'redux-form';
 import { RootState } from 'store/store';
 import { getUserUuid } from "common/getuser";
 import { dialogActions } from "store/dialog/dialog-actions";
@@ -15,15 +15,24 @@ import { progressIndicatorActions } from "store/progress-indicator/progress-indi
 import { isProjectOrRunProcessRoute } from 'store/projects/project-create-actions';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { CollectionResource } from "models/collection";
+import { ResourcePropertiesFormData } from "views-components/resource-properties-form/resource-properties-form";
+import { addProperty, deleteProperty } from "lib/resource-properties";
 
 export interface CollectionCreateFormDialogData {
     ownerUuid: string;
     name: string;
     description: string;
     storageClassesDesired: string[];
+    properties: CollectionProperties;
+}
+
+export interface CollectionProperties {
+    [key: string]: string | string[];
 }
 
 export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName";
+export const COLLECTION_CREATE_PROPERTIES_FORM_NAME = "collectionCreatePropertiesFormName";
+export const COLLECTION_CREATE_FORM_SELECTOR = formValueSelector(COLLECTION_CREATE_FORM_NAME);
 
 export const openCollectionCreateDialog = (ownerUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -69,3 +78,23 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
         }
     };
+
+export const addPropertyToCreateCollectionForm = (data: ResourcePropertiesFormData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...COLLECTION_CREATE_FORM_SELECTOR(getState(), 'properties') };
+        const key = data.keyID || data.key;
+        const value =  data.valueID || data.value;
+        dispatch(change(
+            COLLECTION_CREATE_FORM_NAME,
+            'properties',
+            addProperty(properties, key, value)));
+    };
+
+export const removePropertyFromCreateCollectionForm = (key: string, value: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...COLLECTION_CREATE_FORM_SELECTOR(getState(), 'properties') };
+        dispatch(change(
+            COLLECTION_CREATE_FORM_NAME,
+            'properties',
+            deleteProperty(properties, key, value)));
+    };
index 04f42b8d82f033b8a4d87690bfa988976ad06c40..0096bc4828c1642926c3f00c0a4fa3b604a22935 100644 (file)
@@ -3,7 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form';
+import {
+    change,
+    FormErrors,
+    formValueSelector,
+    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";
@@ -15,15 +22,21 @@ import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { updateResources } from "../resources/resources-actions";
 import { loadDetailsPanel } from "../details-panel/details-panel-action";
 import { getResource } from "store/resources/resources";
+import { CollectionProperties } from "./collection-create-actions";
+import { ResourcePropertiesFormData } from "views-components/resource-properties-form/resource-properties-form";
+import { addProperty, deleteProperty } from "lib/resource-properties";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
     name: string;
     description?: string;
     storageClassesDesired?: string[];
+    properties?: CollectionProperties;
 }
 
 export const COLLECTION_UPDATE_FORM_NAME = 'collectionUpdateFormName';
+export const COLLECTION_UPDATE_PROPERTIES_FORM_NAME = "collectionCreatePropertiesFormName";
+export const COLLECTION_UPDATE_FORM_SELECTOR = formValueSelector(COLLECTION_UPDATE_FORM_NAME);
 
 export const openCollectionUpdateDialog = (resource: CollectionUpdateFormDialogData) =>
     (dispatch: Dispatch) => {
@@ -41,7 +54,8 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
         services.collectionService.update(uuid, {
             name: collection.name,
             storageClassesDesired: collection.storageClassesDesired,
-            description: collection.description }
+            description: collection.description,
+            properties: collection.properties }
         ).then(updatedCollection => {
             updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
@@ -69,3 +83,23 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
             }
         );
     };
+
+export const addPropertyToUpdateCollectionForm = (data: ResourcePropertiesFormData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...COLLECTION_UPDATE_FORM_SELECTOR(getState(), 'properties') };
+        const key = data.keyID || data.key;
+        const value =  data.valueID || data.value;
+        dispatch(change(
+            COLLECTION_UPDATE_FORM_NAME,
+            'properties',
+            addProperty(properties, key, value)));
+    };
+
+export const removePropertyFromUpdateCollectionForm = (key: string, value: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...COLLECTION_UPDATE_FORM_SELECTOR(getState(), 'properties') };
+        dispatch(change(
+            COLLECTION_UPDATE_FORM_NAME,
+            'properties',
+            deleteProperty(properties, key, value)));
+    };
index 9a8733ba6ffa9ae77db8118f79c4d2c070064f78..38433eb27c6e26ce1fc9dae7ff49b5f9e28250c1 100644 (file)
@@ -41,6 +41,7 @@ export type ContextMenuResource = {
     outputUuid?: string;
     workflowUuid?: string;
     storageClassesDesired?: string[];
+    properties?: { [key: string]: string | string[] };
 };
 
 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
index ba17675380074bf761ceda72e904f596f386955f..e5fc34d8ebe6f3ecc7f5824a0534105e72b42ba0 100644 (file)
@@ -3,23 +3,40 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { FormErrors, initialize, reset, startSubmit, stopSubmit } from 'redux-form';
+import {
+    change,
+    FormErrors,
+    formValueSelector,
+    initialize,
+    reset,
+    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";
+import {
+    getCommonResourceServiceError,
+    CommonResourceServiceError
+} from "services/common-service/common-resource-service";
 import { ServiceRepository } from "services/services";
 import { projectPanelActions } from 'store/project-panel/project-panel-action';
 import { GroupClass } from "models/group";
 import { Participant } from "views-components/sharing-dialog/participant-select";
+import { ResourcePropertiesFormData } from "views-components/resource-properties-form/resource-properties-form";
+import { addProperty, deleteProperty } from "lib/resource-properties";
+import { ProjectProperties } from "./project-create-actions";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
     name: string;
     users?: Participant[];
     description?: string;
+    properties?: ProjectProperties;
 }
 
 export const PROJECT_UPDATE_FORM_NAME = 'projectUpdateFormName';
+export const PROJECT_UPDATE_PROPERTIES_FORM_NAME = 'projectUpdatePropertiesFormName';
+export const PROJECT_UPDATE_FORM_SELECTOR = formValueSelector(PROJECT_UPDATE_FORM_NAME);
 
 export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
@@ -32,7 +49,13 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
         const uuid = project.uuid || '';
         dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
         try {
-            const updatedProject = await services.projectService.update(uuid, { name: project.name, description: project.description });
+            const updatedProject = await services.projectService.update(
+                uuid,
+                {
+                    name: project.name,
+                    description: project.description,
+                    properties: project.properties,
+                });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(reset(PROJECT_UPDATE_FORM_NAME));
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
@@ -45,3 +68,23 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
             return ;
         }
     };
+
+export const addPropertyToUpdateProjectForm = (data: ResourcePropertiesFormData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...PROJECT_UPDATE_FORM_SELECTOR(getState(), 'properties') };
+        const key = data.keyID || data.key;
+        const value =  data.valueID || data.value;
+        dispatch(change(
+            PROJECT_UPDATE_FORM_NAME,
+            'properties',
+            addProperty(properties, key, value)));
+    };
+
+export const removePropertyFromUpdateProjectForm = (key: string, value: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const properties = { ...PROJECT_UPDATE_FORM_SELECTOR(getState(), 'properties') };
+        dispatch(change(
+            PROJECT_UPDATE_FORM_NAME,
+            'properties',
+            deleteProperty(properties, key, value)));
+    };
diff --git a/src/views-components/collection-properties/create-collection-properties-form.tsx b/src/views-components/collection-properties/create-collection-properties-form.tsx
new file mode 100644 (file)
index 0000000..8e3f8eb
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm, reset } from 'redux-form';
+import { withStyles } from '@material-ui/core';
+import {
+    COLLECTION_CREATE_PROPERTIES_FORM_NAME,
+    addPropertyToCreateCollectionForm
+} from 'store/collections/collection-create-actions';
+import {
+    ResourcePropertiesForm,
+    ResourcePropertiesFormData
+} from 'views-components/resource-properties-form/resource-properties-form';
+
+const Form = withStyles(
+    ({ spacing }) => (
+        { container:
+            {
+                paddingTop: spacing.unit,
+                margin: 0,
+            }
+        })
+    )(ResourcePropertiesForm);
+
+export const CreateCollectionPropertiesForm = reduxForm<ResourcePropertiesFormData>({
+    form: COLLECTION_CREATE_PROPERTIES_FORM_NAME,
+    onSubmit: (data, dispatch) => {
+        dispatch(addPropertyToCreateCollectionForm(data));
+        dispatch(reset(COLLECTION_CREATE_PROPERTIES_FORM_NAME));
+    }
+})(Form);
\ No newline at end of file
diff --git a/src/views-components/collection-properties/create-collection-properties-list.tsx b/src/views-components/collection-properties/create-collection-properties-list.tsx
new file mode 100644 (file)
index 0000000..9784b55
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import {
+    withStyles,
+    StyleRulesCallback,
+    WithStyles,
+} from '@material-ui/core';
+import { RootState } from 'store/store';
+import {
+    removePropertyFromCreateCollectionForm,
+    COLLECTION_CREATE_FORM_SELECTOR,
+    CollectionProperties
+} from 'store/collections/collection-create-actions';
+import { ArvadosTheme } from 'common/custom-theme';
+import { getPropertyChip } from '../resource-properties-form/property-chip';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    }
+});
+
+interface CreateCollectionPropertiesListDataProps {
+    properties: CollectionProperties;
+}
+
+interface CreateCollectionPropertiesListActionProps {
+    handleDelete: (key: string, value: string) => void;
+}
+
+const mapStateToProps = (state: RootState): CreateCollectionPropertiesListDataProps => {
+    const properties = COLLECTION_CREATE_FORM_SELECTOR(state, 'properties');
+    return { properties };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): CreateCollectionPropertiesListActionProps => ({
+    handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromCreateCollectionForm(key, value))
+});
+
+type CreateCollectionPropertiesListProps = CreateCollectionPropertiesListDataProps &
+    CreateCollectionPropertiesListActionProps & WithStyles<CssRules>;
+
+const List = withStyles(styles)(
+    ({ classes, handleDelete, properties }: CreateCollectionPropertiesListProps) =>
+        <div>
+            {properties &&
+                Object.keys(properties).map(k =>
+                    Array.isArray(properties[k])
+                    ? (properties[k] as string[]).map((v: string) =>
+                        getPropertyChip(
+                            k, v,
+                            () => handleDelete(k, v),
+                            classes.tag))
+                    : getPropertyChip(
+                        k, (properties[k] as string),
+                        () => handleDelete(k, (properties[k] as string)),
+                        classes.tag))
+                }
+        </div>
+);
+
+export const CreateCollectionPropertiesList = connect(mapStateToProps, mapDispatchToProps)(List);
\ No newline at end of file
diff --git a/src/views-components/collection-properties/update-collection-properties-form.tsx b/src/views-components/collection-properties/update-collection-properties-form.tsx
new file mode 100644 (file)
index 0000000..dc00de3
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm, reset } from 'redux-form';
+import { withStyles } from '@material-ui/core';
+import {
+    addPropertyToUpdateCollectionForm,
+    COLLECTION_UPDATE_PROPERTIES_FORM_NAME
+} from 'store/collections/collection-update-actions';
+import {
+    ResourcePropertiesForm,
+    ResourcePropertiesFormData
+} from 'views-components/resource-properties-form/resource-properties-form';
+
+const Form = withStyles(
+    ({ spacing }) => (
+        { container:
+            {
+                paddingTop: spacing.unit,
+                margin: 0,
+            }
+        })
+    )(ResourcePropertiesForm);
+
+export const UpdateCollectionPropertiesForm = reduxForm<ResourcePropertiesFormData>({
+    form: COLLECTION_UPDATE_PROPERTIES_FORM_NAME,
+    onSubmit: (data, dispatch) => {
+        dispatch(addPropertyToUpdateCollectionForm(data));
+        dispatch(reset(COLLECTION_UPDATE_PROPERTIES_FORM_NAME));
+    }
+})(Form);
\ No newline at end of file
diff --git a/src/views-components/collection-properties/update-collection-properties-list.tsx b/src/views-components/collection-properties/update-collection-properties-list.tsx
new file mode 100644 (file)
index 0000000..26cf5e7
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import {
+    withStyles,
+    StyleRulesCallback,
+    WithStyles,
+} from '@material-ui/core';
+import { RootState } from 'store/store';
+import {
+    removePropertyFromUpdateCollectionForm,
+    COLLECTION_UPDATE_FORM_SELECTOR,
+} from 'store/collections/collection-update-actions';
+import { ArvadosTheme } from 'common/custom-theme';
+import { getPropertyChip } from '../resource-properties-form/property-chip';
+import { CollectionProperties } from 'store/collections/collection-create-actions';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    }
+});
+
+interface UpdateCollectionPropertiesListDataProps {
+    properties: CollectionProperties;
+}
+
+interface UpdateCollectionPropertiesListActionProps {
+    handleDelete: (key: string, value: string) => void;
+}
+
+const mapStateToProps = (state: RootState): UpdateCollectionPropertiesListDataProps => {
+    const properties = COLLECTION_UPDATE_FORM_SELECTOR(state, 'properties');
+    return { properties };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): UpdateCollectionPropertiesListActionProps => ({
+    handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromUpdateCollectionForm(key, value))
+});
+
+type UpdateCollectionPropertiesListProps = UpdateCollectionPropertiesListDataProps &
+    UpdateCollectionPropertiesListActionProps & WithStyles<CssRules>;
+
+const List = withStyles(styles)(
+    ({ classes, handleDelete, properties }: UpdateCollectionPropertiesListProps) =>
+        <div>
+            {properties &&
+                Object.keys(properties).map(k =>
+                    Array.isArray(properties[k])
+                    ? (properties[k] as string[]).map((v: string) =>
+                        getPropertyChip(
+                            k, v,
+                            () => handleDelete(k, v),
+                            classes.tag))
+                    : getPropertyChip(
+                        k, (properties[k] as string),
+                        () => handleDelete(k, (properties[k] as string)),
+                        classes.tag))
+                }
+        </div>
+);
+
+export const UpdateCollectionPropertiesList = connect(mapStateToProps, mapDispatchToProps)(List);
\ No newline at end of file
index c85a6d121506062307340e94017f32bc9dec7760..b75ad50f7cf10fedbbebab81947f72961f62d565 100644 (file)
@@ -14,6 +14,9 @@ import {
 } from 'views-components/form-fields/collection-form-fields';
 import { FileUploaderField } from '../file-uploader/file-uploader';
 import { ResourceParentField } from '../form-fields/resource-form-fields';
+import { CreateCollectionPropertiesList } from 'views-components/collection-properties/create-collection-properties-list';
+import { CreateCollectionPropertiesForm } from 'views-components/collection-properties/create-collection-properties-form';
+import { FormGroup, FormLabel } from '@material-ui/core';
 
 type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
@@ -29,6 +32,11 @@ const CollectionAddFields = () => <span>
     <ResourceParentField />
     <CollectionNameField />
     <CollectionDescriptionField />
+    <FormLabel>Properties</FormLabel>
+    <FormGroup>
+        <CreateCollectionPropertiesForm />
+        <CreateCollectionPropertiesList />
+    </FormGroup>
     <CollectionStorageClassesField defaultClasses={['default']} />
     <Field
         name='files'
index 85a2380e9d1029350c27c64d9a197dc2ff8bfb5a..81e9485c12ef9819eca731f99f58ce127c16c8c5 100644 (file)
@@ -11,6 +11,7 @@ import { ProjectNameField, ProjectDescriptionField } from 'views-components/form
 import { CreateProjectPropertiesForm } from 'views-components/project-properties/create-project-properties-form';
 import { CreateProjectPropertiesList } from 'views-components/project-properties/create-project-properties-list';
 import { ResourceParentField } from '../form-fields/resource-form-fields';
+import { FormGroup, FormLabel } from '@material-ui/core';
 
 type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
 
@@ -26,6 +27,9 @@ const ProjectAddFields = () => <span>
     <ResourceParentField />
     <ProjectNameField />
     <ProjectDescriptionField />
-    <CreateProjectPropertiesForm />
-    <CreateProjectPropertiesList />
+    <FormLabel>Properties</FormLabel>
+    <FormGroup>
+        <CreateProjectPropertiesForm />
+        <CreateProjectPropertiesList />
+    </FormGroup>
 </span>;
index 7ef6e4b3cd4b3b048bfa01b59801fb2b282f93a7..d989d431fa649ac064420823090788290e943c55 100644 (file)
@@ -17,7 +17,13 @@ export const CreateCollectionDialog = compose(
         onSubmit: (data, dispatch) => {
             // Somehow an extra field called 'files' gets added, copy
             // the data object to get rid of it.
-            dispatch(createCollection({ ownerUuid: data.ownerUuid, name: data.name, description: data.description, storageClassesDesired: data.storageClassesDesired }));
+            dispatch(createCollection({
+                ownerUuid: data.ownerUuid,
+                name: data.name,
+                description: data.description,
+                storageClassesDesired: data.storageClassesDesired,
+                properties: data.properties,
+            }));
         }
     })
 )(DialogCollectionCreate);
index cce64d27ac8a987449b4714abbb8d6b1e62249bb..5bdaa4f408e659a390d03152ecb7316fffbf2a87 100644 (file)
@@ -12,6 +12,9 @@ import {
     CollectionDescriptionField,
     CollectionStorageClassesField
 } from 'views-components/form-fields/collection-form-fields';
+import { UpdateCollectionPropertiesForm } from 'views-components/collection-properties/update-collection-properties-form';
+import { UpdateCollectionPropertiesList } from 'views-components/collection-properties/update-collection-properties-list';
+import { FormGroup, FormLabel } from '@material-ui/core';
 
 type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionUpdateFormDialogData>;
 
@@ -26,5 +29,10 @@ export const DialogCollectionUpdate = (props: DialogCollectionProps) =>
 const CollectionEditFields = () => <span>
     <CollectionNameField />
     <CollectionDescriptionField />
+    <FormLabel>Properties</FormLabel>
+    <FormGroup>
+        <UpdateCollectionPropertiesForm />
+        <UpdateCollectionPropertiesList />
+    </FormGroup>
     <CollectionStorageClassesField />
 </span>;
index fda7c47d7d33c72dd1766d1d5bc5aa2a050c167a..96e6d927751d092ee48af40d65a3512241ce1475 100644 (file)
@@ -9,6 +9,9 @@ import { ProjectUpdateFormDialogData } from 'store/projects/project-update-actio
 import { FormDialog } from 'components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField, UsersField } from 'views-components/form-fields/project-form-fields';
 import { GroupClass } from 'models/group';
+import { FormGroup, FormLabel } from '@material-ui/core';
+import { UpdateProjectPropertiesForm } from 'views-components/project-properties/update-project-properties-form';
+import { UpdateProjectPropertiesList } from 'views-components/project-properties/update-project-properties-list';
 
 type DialogProjectProps = WithDialogProps<{sourcePanel: GroupClass, create?: boolean}> & InjectedFormProps<ProjectUpdateFormDialogData>;
 
@@ -35,6 +38,11 @@ export const DialogProjectUpdate = (props: DialogProjectProps) => {
 const ProjectEditFields = () => <span>
     <ProjectNameField />
     <ProjectDescriptionField />
+    <FormLabel>Properties</FormLabel>
+    <FormGroup>
+        <UpdateProjectPropertiesForm />
+        <UpdateProjectPropertiesList />
+    </FormGroup>
 </span>;
 
 const GroupAddFields = () => <span>
diff --git a/src/views-components/project-properties/update-project-properties-form.tsx b/src/views-components/project-properties/update-project-properties-form.tsx
new file mode 100644 (file)
index 0000000..e6e78e3
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm, reset } from 'redux-form';
+import { withStyles } from '@material-ui/core';
+import {
+    PROJECT_UPDATE_PROPERTIES_FORM_NAME,
+    addPropertyToUpdateProjectForm
+} from 'store/projects/project-update-actions';
+import {
+    ResourcePropertiesForm,
+    ResourcePropertiesFormData
+} from 'views-components/resource-properties-form/resource-properties-form';
+
+const Form = withStyles(
+    ({ spacing }) => (
+        { container:
+            {
+                paddingTop: spacing.unit,
+                margin: 0,
+            }
+        })
+    )(ResourcePropertiesForm);
+
+export const UpdateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>({
+    form: PROJECT_UPDATE_PROPERTIES_FORM_NAME,
+    onSubmit: (data, dispatch) => {
+        dispatch(addPropertyToUpdateProjectForm(data));
+        dispatch(reset(PROJECT_UPDATE_PROPERTIES_FORM_NAME));
+    }
+})(Form);
\ No newline at end of file
diff --git a/src/views-components/project-properties/update-project-properties-list.tsx b/src/views-components/project-properties/update-project-properties-list.tsx
new file mode 100644 (file)
index 0000000..5572af7
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import {
+    withStyles,
+    StyleRulesCallback,
+    WithStyles,
+} from '@material-ui/core';
+import { RootState } from 'store/store';
+import {
+    removePropertyFromUpdateProjectForm,
+    PROJECT_UPDATE_FORM_SELECTOR,
+} from 'store/projects/project-update-actions';
+import { ArvadosTheme } from 'common/custom-theme';
+import { getPropertyChip } from '../resource-properties-form/property-chip';
+import { ProjectProperties } from 'store/projects/project-create-actions';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    }
+});
+
+interface UpdateProjectPropertiesListDataProps {
+    properties: ProjectProperties;
+}
+
+interface UpdateProjectPropertiesListActionProps {
+    handleDelete: (key: string, value: string) => void;
+}
+
+const mapStateToProps = (state: RootState): UpdateProjectPropertiesListDataProps => {
+    const properties = PROJECT_UPDATE_FORM_SELECTOR(state, 'properties');
+    return { properties };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): UpdateProjectPropertiesListActionProps => ({
+    handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromUpdateProjectForm(key, value))
+});
+
+type UpdateProjectPropertiesListProps = UpdateProjectPropertiesListDataProps &
+    UpdateProjectPropertiesListActionProps & WithStyles<CssRules>;
+
+const List = withStyles(styles)(
+    ({ classes, handleDelete, properties }: UpdateProjectPropertiesListProps) =>
+        <div>
+            {properties &&
+                Object.keys(properties).map(k =>
+                    Array.isArray(properties[k])
+                    ? (properties[k] as string[]).map((v: string) =>
+                        getPropertyChip(
+                            k, v,
+                            () => handleDelete(k, v),
+                            classes.tag))
+                    : getPropertyChip(
+                        k, (properties[k] as string),
+                        () => handleDelete(k, (properties[k] as string)),
+                        classes.tag))
+                }
+        </div>
+);
+
+export const UpdateProjectPropertiesList = connect(mapStateToProps, mapDispatchToProps)(List);
\ No newline at end of file
index adc3e9959418926500276f2ba99f8d5290dabe6f..851008c053228443cf40bfd3a3aa3cff3ec088f3 100644 (file)
@@ -220,7 +220,8 @@ export const CollectionPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {
-                const { uuid, ownerUuid, name, description, kind, storageClassesDesired } = this.props.item;
+                const { uuid, ownerUuid, name, description,
+                    kind, storageClassesDesired, properties } = this.props.item;
                 const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(uuid));
                 const resource = {
                     uuid,
@@ -230,6 +231,7 @@ export const CollectionPanel = withStyles(styles)(
                     storageClassesDesired,
                     kind,
                     menuKind,
+                    properties,
                 };
                 // Avoid expanding/collapsing the panel
                 event.stopPropagation();
index 4a3f60a619badef42c72e3b5ed5a56ba97868f3a..e08aea32217016ae6448f8cb55e87640e8d12860 100644 (file)
@@ -185,6 +185,7 @@ export const ProjectPanel = withStyles(styles)(
                         menuKind,
                         description: resource.description,
                         storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
+                        properties: ('properties' in resource) ? resource.properties : {},
                     }));
                 }
                 this.props.dispatch<any>(loadDetailsPanel(resourceUuid));