From: Lucas Di Pentima Date: Wed, 5 Jan 2022 21:48:34 +0000 (-0300) Subject: Merge branch '18219-props-in-collection-details-panel'. Closes #18219 X-Git-Tag: 2.4.0~21 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/b130d8595d05d46f2f29ebe4c1cf3dff392bc26b?hp=58db72fee358d5987139a1b8526c0ca873e07dbf Merge branch '18219-props-in-collection-details-panel'. Closes #18219 Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 82a26cef..51933887 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -75,7 +75,7 @@ describe('Collection panel tests', function () { }); }); - it('uses the property editor with vocabulary terms', function () { + it('uses the property editor (from edit dialog) with vocabulary terms', function () { cy.createCollection(adminUser.token, { name: `Test collection ${Math.floor(Math.random() * 999999)}`, owner_uuid: activeUser.user.uuid, @@ -85,6 +85,14 @@ describe('Collection panel tests', function () { cy.loginAs(activeUser); cy.goToPath(`/collections/${this.testCollection.uuid}`); + cy.get('[data-cy=collection-info-panel') + .should('contain', this.testCollection.name) + .and('not.contain', 'Color: Magenta'); + + cy.get('[data-cy=collection-panel-options-btn]').click(); + cy.get('[data-cy=context-menu]').contains('Edit collection').click(); + cy.get('[data-cy=form-dialog]').should('contain', 'Properties'); + // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3) cy.get('[data-cy=resource-properties-form]').within(() => { cy.get('[data-cy=property-field-key]').within(() => { @@ -96,14 +104,56 @@ describe('Collection panel tests', function () { cy.root().submit(); }); // Confirm proper vocabulary labels are displayed on the UI. - cy.get('[data-cy=collection-properties-panel]') - .should('contain', 'Color: Magenta'); + cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta'); + cy.get('[data-cy=form-dialog]').contains('Save').click(); + cy.get('[data-cy=form-dialog]').should('not.exist'); // Confirm proper vocabulary IDs were saved on the backend. cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`) .its('body').as('collection') .then(function () { expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3'); }); + // Confirm the property is displayed on the UI. + cy.get('[data-cy=collection-info-panel') + .should('contain', this.testCollection.name) + .and('contain', 'Color: Magenta'); + }); + }); + + it('uses the editor (from details panel) with vocabulary terms', function () { + cy.createCollection(adminUser.token, { + name: `Test collection ${Math.floor(Math.random() * 999999)}`, + owner_uuid: activeUser.user.uuid, + manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n" + }) + .as('testCollection').then(function () { + cy.loginAs(activeUser); + cy.goToPath(`/collections/${this.testCollection.uuid}`); + + cy.get('[data-cy=collection-info-panel') + .should('contain', this.testCollection.name) + .and('not.contain', 'Color: Magenta') + .and('not.contain', 'Size: S'); + cy.get('[data-cy=additional-info-icon]').click(); + + cy.get('[data-cy=details-panel]').within(() => { + cy.get('[data-cy=details-panel-edit-btn]').click(); + }); + cy.get('[data-cy=form-dialog').contains('Edit Collection'); + + // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3) + cy.get('[data-cy=resource-properties-form]').within(() => { + cy.get('[data-cy=property-field-key]').within(() => { + cy.get('input').type('Color'); + }); + cy.get('[data-cy=property-field-value]').within(() => { + cy.get('input').type('Magenta'); + }); + cy.root().submit(); + }); + // Confirm proper vocabulary labels are displayed on the UI. + cy.get('[data-cy=form-dialog]') + .should('contain', 'Color: Magenta'); // Case-insensitive on-blur auto-selection test // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2) @@ -120,14 +170,25 @@ describe('Collection panel tests', function () { cy.root().submit(); }); // Confirm proper vocabulary labels are displayed on the UI. - cy.get('[data-cy=collection-properties-panel]') + cy.get('[data-cy=form-dialog]') .should('contain', 'Size: S'); + + cy.get('[data-cy=form-dialog]').contains('Save').click(); + cy.get('[data-cy=form-dialog]').should('not.exist'); + // Confirm proper vocabulary IDs were saved on the backend. cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`) .its('body').as('collection') .then(function () { + expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3'); expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2'); }); + + // Confirm properties display on the UI. + cy.get('[data-cy=collection-info-panel') + .should('contain', this.testCollection.name) + .and('contain', 'Color: Magenta') + .and('contain', 'Size: S'); }); }); @@ -186,29 +247,9 @@ describe('Collection panel tests', function () { .should('contain', 'Add to favorites') .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection'); cy.get('body').click(); // Collapse the menu avoiding details panel expansion - cy.get('[data-cy=collection-properties-panel]') - .should('contain', 'someKey') - .and('contain', 'someValue') - .and('not.contain', 'anotherKey') - .and('not.contain', 'anotherValue') - if (isWritable === true) { - // Check that properties can be added. - cy.get('[data-cy=resource-properties-form]').within(() => { - cy.get('[data-cy=property-field-key]').within(() => { - cy.get('input').type('anotherKey'); - }); - cy.get('[data-cy=property-field-value]').within(() => { - cy.get('input').type('anotherValue'); - }); - cy.root().submit(); - }) - cy.get('[data-cy=collection-properties-panel]') - .should('contain', 'anotherKey') - .and('contain', 'anotherValue') - } else { - // Properties form shouldn't be displayed. - cy.get('[data-cy=resource-properties-form]').should('not.exist'); - } + cy.get('[data-cy=collection-info-panel]') + .should('contain', 'someKey: someValue') + .and('not.contain', 'anotherKey: anotherValue'); // Check that the file listing show both read & write operations cy.get('[data-cy=collection-files-panel]').within(() => { cy.wait(1000); @@ -313,63 +354,6 @@ describe('Collection panel tests', function () { }); }); - it.skip('renames a file to a different directory', function () { - // Creates the collection using the admin token so we can set up - // a bogus manifest text without block signatures. - cy.createCollection(adminUser.token, { - name: `Test collection ${Math.floor(Math.random() * 999999)}`, - owner_uuid: activeUser.user.uuid, - manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n" - }) - .as('testCollection').then(function () { - cy.loginAs(activeUser); - cy.goToPath(`/collections/${this.testCollection.uuid}`); - - ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => { - cy.get('[data-cy=collection-files-panel]') - .contains('bar').rightclick({force: true}); - cy.get('[data-cy=context-menu]') - .contains('Rename') - .click(); - cy.get('[data-cy=form-dialog]') - .should('contain', 'Rename') - .within(() => { - cy.get('input').type(`{selectall}{backspace}${subdir}/foo`); - }); - cy.get('[data-cy=form-submit-btn]').click(); - cy.get('[data-cy=collection-files-panel]') - .should('not.contain', 'bar') - .and('contain', subdir); - // Look for the "arrow icon" and expand the "subdir" directory. - cy.get('[data-cy=virtual-file-tree] > div > i').click(); - // Rename 'subdir/foo' to 'foo' - cy.get('[data-cy=collection-files-panel]') - .contains('foo').rightclick(); - cy.get('[data-cy=context-menu]') - .contains('Rename') - .click(); - cy.get('[data-cy=form-dialog]') - .should('contain', 'Rename') - .within(() => { - cy.get('input') - .should('have.value', `${subdir}/foo`) - .type(`{selectall}{backspace}bar`); - }); - cy.get('[data-cy=form-submit-btn]').click(); - cy.get('[data-cy=collection-files-panel]') - .should('contain', subdir) // empty dir kept - .and('contain', 'bar'); - - cy.get('[data-cy=collection-files-panel]') - .contains(subdir).rightclick(); - cy.get('[data-cy=context-menu]') - .contains('Remove') - .click(); - cy.get('[data-cy=confirmation-dialog-ok-btn]').click(); - }); - }); - }); - it('renames a file to a different directory', function () { // Creates the collection using the admin token so we can set up // a bogus manifest text without block signatures. @@ -807,7 +791,7 @@ describe('Collection panel tests', function () { }); }); - it('creates new collection on home project', function () { + it('creates new collection with properties on home project', function () { cy.loginAs(activeUser); cy.goToPath(`/projects/${activeUser.user.uuid}`); cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects'); @@ -817,6 +801,8 @@ describe('Collection panel tests', function () { cy.get('[data-cy=side-panel-new-collection]').click(); // Name between brackets tests bugfix #17582 const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`; + + // Select a storage class. cy.get('[data-cy=form-dialog]') .should('contain', 'New collection') .and('contain', 'Storage classes') @@ -832,15 +818,42 @@ describe('Collection panel tests', function () { }); cy.get('[data-cy=checkbox-foo]').click(); }) + + // Add a property. + // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3) + cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta'); + cy.get('[data-cy=resource-properties-form]').within(() => { + cy.get('[data-cy=property-field-key]').within(() => { + cy.get('input').type('Color'); + }); + cy.get('[data-cy=property-field-value]').within(() => { + cy.get('input').type('Magenta'); + }); + cy.root().submit(); + }); + // Confirm proper vocabulary labels are displayed on the UI. + cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta'); + cy.get('[data-cy=form-submit-btn]').click(); - // Confirm that the user was taken to the newly created thing + // Confirm that the user was taken to the newly created collection cy.get('[data-cy=form-dialog]').should('not.exist'); cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects'); cy.get('[data-cy=breadcrumb-last]').should('contain', collName); cy.get('[data-cy=collection-info-panel]') .should('contain', 'default') .and('contain', 'foo') + .and('contain', 'Color: Magenta') .and('not.contain', 'bar'); + // Confirm that the collection's properties has the real values. + cy.doRequest('GET', '/arvados/v1/collections', null, { + filters: `[["name", "=", "${collName}"]]`, + }) + .its('body.items').as('collections') + .then(function() { + expect(this.collections).to.have.lengthOf(1); + expect(this.collections[0].properties).to.have.property( + 'IDTAGCOLORS', 'IDVALCOLORS3'); + }); }); it('shows responsible person for collection if available', () => { diff --git a/cypress/integration/group-manage.spec.js b/cypress/integration/group-manage.spec.js index c98c2201..84822034 100644 --- a/cypress/integration/group-manage.spec.js +++ b/cypress/integration/group-manage.spec.js @@ -36,11 +36,6 @@ describe('Group manage tests', function() { ); }); - beforeEach(function() { - cy.clearCookies(); - cy.clearLocalStorage(); - }); - it('creates a new group', function() { cy.loginAs(activeUser); @@ -50,14 +45,16 @@ describe('Group manage tests', function() { // Create new group cy.get('[data-cy=groups-panel-new-group]').click(); cy.get('[data-cy=form-dialog]') - .should('contain', 'Create Group') + .should('contain', 'New Group') .within(() => { cy.get('input[name=name]').type(groupName); cy.get('[data-cy=users-field] input').type("three"); }); cy.get('[role=tooltip]').click(); - cy.get('[data-cy=form-dialog] button[type=submit]').click(); - + cy.get('[data-cy=form-dialog]').within(() => { + cy.get('[data-cy=form-submit-btn]').click(); + }) + // Check that the group was created cy.get('[data-cy=groups-panel-data-explorer]').contains(groupName).click(); cy.get('[data-cy=group-members-data-explorer]').contains(activeUser.user.full_name); @@ -230,7 +227,7 @@ describe('Group manage tests', function() { .should('contain', 'Edit Group') .within(() => { cy.get('input[name=name]').clear().type(groupName + ' (renamed)'); - cy.get('button[type=submit]').click(); + cy.get('button').contains('Save').click(); }); // Check that the group was renamed diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js index b3d6bbed..6b87a3c2 100644 --- a/cypress/integration/project.spec.js +++ b/cypress/integration/project.spec.js @@ -28,13 +28,13 @@ describe('Project tests', function() { cy.clearLocalStorage(); }); - it('adds creates a new project with properties', function() { + it('creates a new project with properties', function() { const projName = `Test project (${Math.floor(999999 * Math.random())})`; cy.loginAs(activeUser); cy.get('[data-cy=side-panel-button]').click(); cy.get('[data-cy=side-panel-new-project]').click(); cy.get('[data-cy=form-dialog]') - .should('contain', 'New project') + .should('contain', 'New Project') .within(() => { cy.get('[data-cy=name-field]').within(() => { cy.get('input').type(projName); @@ -42,6 +42,7 @@ describe('Project tests', function() { }); // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3) + cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta'); cy.get('[data-cy=resource-properties-form]').within(() => { cy.get('[data-cy=property-field-key]').within(() => { cy.get('input').type('Color'); @@ -73,7 +74,7 @@ describe('Project tests', function() { cy.get('[data-cy=side-panel-button]').click(); cy.get('[data-cy=side-panel-new-project]').click(); cy.get('[data-cy=form-dialog]') - .should('contain', 'New project') + .should('contain', 'New Project') .within(() => { cy.get('[data-cy=parent-field]').within(() => { cy.get('input').invoke('val').then((val) => { diff --git a/src/components/multi-panel-view/multi-panel-view.test.tsx b/src/components/multi-panel-view/multi-panel-view.test.tsx index 6cf13d78..d690e82f 100644 --- a/src/components/multi-panel-view/multi-panel-view.test.tsx +++ b/src/components/multi-panel-view/multi-panel-view.test.tsx @@ -10,7 +10,7 @@ import { Button } from "@material-ui/core"; configure({ adapter: new Adapter() }); -const PanelMock = ({panelName, panelMaximized, doHidePanel, doMaximizePanel, children, ...rest}) => +const PanelMock = ({panelName, panelMaximized, doHidePanel, doMaximizePanel, panelIlluminated, panelRef, children, ...rest}) =>
{children}
; describe('', () => { diff --git a/src/components/multi-panel-view/multi-panel-view.tsx b/src/components/multi-panel-view/multi-panel-view.tsx index dbb37921..185c3b90 100644 --- a/src/components/multi-panel-view/multi-panel-view.tsx +++ b/src/components/multi-panel-view/multi-panel-view.tsx @@ -64,6 +64,7 @@ interface MPVPanelDataProps { panelMaximized?: boolean; panelIlluminated?: boolean; panelRef?: MutableRefObject; + forwardProps?: boolean; } interface MPVPanelActionProps { @@ -77,7 +78,7 @@ export type MPVPanelProps = MPVPanelDataProps & MPVPanelActionProps; type MPVPanelContentProps = {children: ReactElement} & MPVPanelProps & GridProps; // Grid item compatible component for layout and MPV props passing -export const MPVPanelContent = ({doHidePanel, doMaximizePanel, panelName, panelMaximized, panelIlluminated, panelRef, ...props}: MPVPanelContentProps) => { +export const MPVPanelContent = ({doHidePanel, doMaximizePanel, panelName, panelMaximized, panelIlluminated, panelRef, forwardProps, ...props}: MPVPanelContentProps) => { useEffect(() => { if (panelRef && panelRef.current) { panelRef.current.scrollIntoView({behavior: 'smooth'}); @@ -87,7 +88,9 @@ export const MPVPanelContent = ({doHidePanel, doMaximizePanel, panelName, panelM return {/* Element to scroll to when the panel is selected */} - {React.cloneElement(props.children, { doHidePanel, doMaximizePanel, panelName, panelMaximized })} + { forwardProps + ? React.cloneElement(props.children, { doHidePanel, doMaximizePanel, panelName, panelMaximized }) + : props.children } ; } diff --git a/src/models/collection.ts b/src/models/collection.ts index baa25c7a..3effe672 100644 --- a/src/models/collection.ts +++ b/src/models/collection.ts @@ -2,13 +2,16 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { ResourceKind, TrashableResource } from "./resource"; +import { + ResourceKind, + TrashableResource, + ResourceWithProperties +} from "./resource"; -export interface CollectionResource extends TrashableResource { +export interface CollectionResource extends TrashableResource, ResourceWithProperties { kind: ResourceKind.COLLECTION; name: string; description: string; - properties: any; portableDataHash: string; manifestText: string; replicationDesired: number; diff --git a/src/models/container-request.ts b/src/models/container-request.ts index 9a57a41d..99ec4cf0 100644 --- a/src/models/container-request.ts +++ b/src/models/container-request.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Resource, ResourceKind } from "./resource"; +import { Resource, ResourceKind, ResourceWithProperties } from "./resource"; import { MountType } from "models/mount-types"; import { RuntimeConstraints } from './runtime-constraints'; import { SchedulingParameters } from './scheduling-parameters'; @@ -13,11 +13,10 @@ export enum ContainerRequestState { FINAL = "Final" } -export interface ContainerRequestResource extends Resource { +export interface ContainerRequestResource extends Resource, ResourceWithProperties { kind: ResourceKind.CONTAINER_REQUEST; name: string; description: string; - properties: any; state: ContainerRequestState; requestingContainerUuid: string | null; containerUuid: string | null; diff --git a/src/models/group.ts b/src/models/group.ts index a0c22212..3f3656cc 100644 --- a/src/models/group.ts +++ b/src/models/group.ts @@ -2,14 +2,19 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { ResourceKind, TrashableResource, ResourceObjectType, RESOURCE_UUID_REGEX } from "./resource"; +import { + ResourceKind, + ResourceWithProperties, + RESOURCE_UUID_REGEX, + ResourceObjectType, + TrashableResource +} from "./resource"; -export interface GroupResource extends TrashableResource { +export interface GroupResource extends TrashableResource, ResourceWithProperties { kind: ResourceKind.GROUP; name: string; groupClass: GroupClass | null; description: string; - properties: any; writableBy: string[]; ensure_unique_name: boolean; } diff --git a/src/models/link.ts b/src/models/link.ts index 828dced2..f55c5ccf 100644 --- a/src/models/link.ts +++ b/src/models/link.ts @@ -2,16 +2,15 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Resource, ResourceKind } from 'models/resource'; +import { Resource, ResourceKind, ResourceWithProperties } from 'models/resource'; -export interface LinkResource extends Resource { +export interface LinkResource extends Resource, ResourceWithProperties { headUuid: string; headKind: ResourceKind; tailUuid: string; tailKind: string; linkClass: string; name: string; - properties: any; kind: ResourceKind.LINK; } diff --git a/src/models/log.ts b/src/models/log.ts index 55967f88..3397993b 100644 --- a/src/models/log.ts +++ b/src/models/log.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Resource } from "./resource"; +import { Resource, ResourceWithProperties } from "./resource"; import { ResourceKind } from 'models/resource'; export enum LogEventType { @@ -18,11 +18,10 @@ export enum LogEventType { STDERR = 'stderr', } -export interface LogResource extends Resource { +export interface LogResource extends Resource, ResourceWithProperties { kind: ResourceKind.LOG; objectUuid: string; eventAt: string; eventType: string; summary: string; - properties: any; } diff --git a/src/models/resource.ts b/src/models/resource.ts index c94c4b25..fd867277 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -14,6 +14,10 @@ export interface Resource { etag: string; } +export interface ResourceWithProperties extends Resource { + properties: any; +} + export interface EditableResource extends Resource { isEditable: boolean; } diff --git a/src/models/tag.ts b/src/models/tag.ts index f4e5854a..fa36486d 100644 --- a/src/models/tag.ts +++ b/src/models/tag.ts @@ -10,6 +10,7 @@ export interface TagResource extends LinkResource { } export interface TagProperty { + uuid: string; key: string; keyID?: string; value: string; diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts index ee476524..c50ff6a8 100644 --- a/src/store/collection-panel/collection-panel-action.ts +++ b/src/store/collection-panel/collection-panel-action.ts @@ -9,15 +9,12 @@ import { import { CollectionResource } from 'models/collection'; import { RootState } from "store/store"; import { ServiceRepository } from "services/services"; -import { TagProperty } from "models/tag"; import { snackbarActions } from "../snackbar/snackbar-actions"; import { resourcesActions } from "store/resources/resources-actions"; 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 { addProperty, deleteProperty } from "lib/resource-properties"; -import { getResource } from "store/resources/resources"; export const collectionPanelActions = unionize({ SET_COLLECTION: ofType(), @@ -27,8 +24,6 @@ export const collectionPanelActions = unionize({ export type CollectionPanelAction = UnionOf; -export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm'; - export const loadCollectionPanel = (uuid: string, forceReload = false) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const { collectionPanel: { item } } = getState(); @@ -44,37 +39,6 @@ export const loadCollectionPanel = (uuid: string, forceReload = false) => return collection; }; -export const createCollectionTag = (data: TagProperty) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const item = getState().collectionPanel.item; - if (!item) { return; } - - const properties = Object.assign({}, item.properties); - const key = data.keyID || data.key; - const value = data.valueID || data.value; - const cachedCollection = getResource(item.uuid)(getState().resources); - services.collectionService.update( - item.uuid, { - properties: addProperty(properties, key, value) - } - ).then(updatedCollection => { - updatedCollection = {...cachedCollection, ...updatedCollection}; - dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection)); - dispatch(resourcesActions.SET_RESOURCES([updatedCollection])); - dispatch(snackbarActions.OPEN_SNACKBAR({ - message: "Property has been successfully added.", - hideDuration: 2000, - kind: SnackbarKind.SUCCESS })); - dispatch(loadDetailsPanel(updatedCollection.uuid)); - return updatedCollection; - }).catch (e => - dispatch(snackbarActions.OPEN_SNACKBAR({ - message: e.errors[0], - hideDuration: 2000, - kind: SnackbarKind.ERROR })) - ); - }; - export const navigateToProcess = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { try { @@ -84,29 +48,3 @@ export const navigateToProcess = (uuid: string) => dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This process does not exist!', hideDuration: 2000, kind: SnackbarKind.ERROR })); } }; - -export const deleteCollectionTag = (key: string, value: string) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const item = getState().collectionPanel.item; - if (!item) { return; } - - const properties = Object.assign({}, item.properties); - const cachedCollection = getResource(item.uuid)(getState().resources); - services.collectionService.update( - item.uuid, { - properties: deleteProperty(properties, key, value) - } - ).then(updatedCollection => { - updatedCollection = {...cachedCollection, ...updatedCollection}; - dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection)); - dispatch(resourcesActions.SET_RESOURCES([updatedCollection])); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Tag has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); - dispatch(loadDetailsPanel(updatedCollection.uuid)); - return updatedCollection; - }).catch (e => { - dispatch(snackbarActions.OPEN_SNACKBAR({ - message: e.errors[0], - hideDuration: 2000, - kind: SnackbarKind.ERROR })); - }); - }; diff --git a/src/store/collections/collection-create-actions.ts b/src/store/collections/collection-create-actions.ts index 81d8948c..17fecc1e 100644 --- a/src/store/collections/collection-create-actions.ts +++ b/src/store/collections/collection-create-actions.ts @@ -3,7 +3,14 @@ // 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, + formValueSelector +} from 'redux-form'; import { RootState } from 'store/store'; import { getUserUuid } from "common/getuser"; import { dialogActions } from "store/dialog/dialog-actions"; @@ -21,9 +28,16 @@ export interface CollectionCreateFormDialogData { 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) => { diff --git a/src/store/collections/collection-update-actions.ts b/src/store/collections/collection-update-actions.ts index 04f42b8d..82418d27 100644 --- a/src/store/collections/collection-update-actions.ts +++ b/src/store/collections/collection-update-actions.ts @@ -3,7 +3,13 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form'; +import { + 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 +21,19 @@ 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"; 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 = "collectionUpdatePropertiesFormName"; +export const COLLECTION_UPDATE_FORM_SELECTOR = formValueSelector(COLLECTION_UPDATE_FORM_NAME); export const openCollectionUpdateDialog = (resource: CollectionUpdateFormDialogData) => (dispatch: Dispatch) => { @@ -41,7 +51,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 })); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 9a8733ba..38433eb2 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -41,6 +41,7 @@ export type ContextMenuResource = { outputUuid?: string; workflowUuid?: string; storageClassesDesired?: string[]; + properties?: { [key: string]: string | string[] }; }; export const isKeyboardClick = (event: React.MouseEvent) => event.nativeEvent.detail === 0; diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts index bda35441..b708ad62 100644 --- a/src/store/details-panel/details-panel-action.ts +++ b/src/store/details-panel/details-panel-action.ts @@ -5,15 +5,10 @@ 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, SnackbarKind} from 'store/snackbar/snackbar-actions'; -import { addProperty, deleteProperty } from 'lib/resource-properties'; +import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions'; import { FilterBuilder } from 'services/api/filter-builder'; import { OrderBuilder } from 'services/api/order-builder'; import { CollectionResource } from 'models/collection'; @@ -29,9 +24,6 @@ export const detailsPanelActions = unionize({ export type DetailsPanelAction = UnionOf; -export const PROJECT_PROPERTIES_FORM_NAME = 'projectPropertiesFormName'; -export const PROJECT_PROPERTIES_DIALOG_NAME = 'projectPropertiesDialogName'; - export const loadDetailsPanel = (uuid: string) => (dispatch: Dispatch, getState: () => RootState) => { if (getState().detailsPanel.isOpened) { @@ -55,11 +47,6 @@ export const openDetailsPanel = (uuid?: string, tabNr: number = 0) => } }; -export const openProjectPropertiesDialog = () => - (dispatch: Dispatch) => { - dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } })); - }; - export const refreshCollectionVersionsList = (uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { services.collectionService.list({ @@ -76,49 +63,6 @@ export const refreshCollectionVersionsList = (uuid: 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; - if (!project) { return; } - - const properties = Object.assign({}, project.properties); - - try { - const updatedProject = await services.projectService.update( - project.uuid, { - properties: deleteProperty(properties, key, value), - }); - dispatch(resourcesActions.SET_RESOURCES([updatedProject])); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); - } catch (e) { - dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors[0], hideDuration: 2000, kind: SnackbarKind.ERROR })); - } - }; - -export const createProjectProperty = (data: TagProperty) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const { detailsPanel, resources } = getState(); - const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource; - if (!project) { return; } - - dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME)); - try { - const key = data.keyID || data.key; - const value = data.valueID || data.value; - const properties = Object.assign({}, project.properties); - const updatedProject = await services.projectService.update( - project.uuid, { - properties: addProperty(properties, key, value), - } - ); - dispatch(resourcesActions.SET_RESOURCES([updatedProject])); - dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); - dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME)); - } catch (e) { - dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors[0], hideDuration: 2000, kind: SnackbarKind.ERROR })); - } - }; export const toggleDetailsPanel = () => (dispatch: Dispatch, getState: () => RootState) => { // because of material-ui issue resizing details panel breaks tabs. // triggering window resize event fixes that. diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts index c72b0017..6e63702e 100644 --- a/src/store/groups-panel/groups-panel-actions.ts +++ b/src/store/groups-panel/groups-panel-actions.ts @@ -16,6 +16,7 @@ import { PermissionLevel } from 'models/permission'; import { PermissionService } from 'services/permission-service/permission-service'; import { FilterBuilder } from 'services/api/filter-builder'; import { ProjectUpdateFormDialogData, PROJECT_UPDATE_FORM_NAME } from 'store/projects/project-update-actions'; +import { PROJECT_CREATE_FORM_NAME } from 'store/projects/project-create-actions'; export const GROUPS_PANEL_ID = "groupsPanel"; @@ -28,8 +29,13 @@ export const loadGroupsPanel = () => GroupsPanelActions.REQUEST_ITEMS(); export const openCreateGroupDialog = () => (dispatch: Dispatch, getState: () => RootState) => { - dispatch(initialize(PROJECT_UPDATE_FORM_NAME, {})); - dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {sourcePanel: GroupClass.ROLE, create: true} })); + dispatch(initialize(PROJECT_CREATE_FORM_NAME, {})); + dispatch(dialogActions.OPEN_DIALOG({ + id: PROJECT_CREATE_FORM_NAME, + data: { + sourcePanel: GroupClass.ROLE, + } + })); }; export const openGroupAttributes = (uuid: string) => @@ -64,7 +70,12 @@ export const openRemoveGroupDialog = (uuid: string) => export const openGroupUpdateDialog = (resource: ProjectUpdateFormDialogData) => (dispatch: Dispatch, getState: () => RootState) => { dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource)); - dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {sourcePanel: GroupClass.ROLE} })); + dispatch(dialogActions.OPEN_DIALOG({ + id: PROJECT_UPDATE_FORM_NAME, + data: { + sourcePanel: GroupClass.ROLE, + } + })); }; export const updateGroup = (project: ProjectUpdateFormDialogData) => @@ -89,7 +100,7 @@ export const updateGroup = (project: ProjectUpdateFormDialogData) => export const createGroup = ({ name, users = [], description }: ProjectUpdateFormDialogData) => async (dispatch: Dispatch, _: {}, { groupsService, permissionService }: ServiceRepository) => { - dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME)); + dispatch(startSubmit(PROJECT_CREATE_FORM_NAME)); try { const newGroup = await groupsService.create({ name, description, groupClass: GroupClass.ROLE }); for (const user of users) { @@ -100,8 +111,8 @@ export const createGroup = ({ name, users = [], description }: ProjectUpdateForm permissionService, }); } - dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME })); - dispatch(reset(PROJECT_UPDATE_FORM_NAME)); + dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME })); + dispatch(reset(PROJECT_CREATE_FORM_NAME)); dispatch(loadGroupsPanel()); dispatch(snackbarActions.OPEN_SNACKBAR({ message: `${newGroup.name} group has been created`, @@ -111,7 +122,7 @@ export const createGroup = ({ name, users = [], description }: ProjectUpdateForm } catch (e) { const error = getCommonResourceServiceError(e); if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) { - dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Group with the same name already exists.' } as FormErrors)); + dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Group with the same name already exists.' } as FormErrors)); } return; } diff --git a/src/store/projects/project-create-actions.ts b/src/store/projects/project-create-actions.ts index 352759fa..23eaf7a4 100644 --- a/src/store/projects/project-create-actions.ts +++ b/src/store/projects/project-create-actions.ts @@ -3,7 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { reset, startSubmit, stopSubmit, initialize, FormErrors, formValueSelector, change } from 'redux-form'; +import { + reset, + startSubmit, + stopSubmit, + initialize, + FormErrors, + formValueSelector +} from 'redux-form'; import { RootState } from 'store/store'; import { getUserUuid } from "common/getuser"; import { dialogActions } from "store/dialog/dialog-actions"; @@ -11,9 +18,8 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from 'servi import { ProjectResource } from 'models/project'; import { ServiceRepository } from 'services/services'; import { matchProjectRoute, matchRunProcessRoute } from 'routes/routes'; -import { ResourcePropertiesFormData } from 'views-components/resource-properties-form/resource-properties-form'; import { RouterState } from "react-router-redux"; -import { addProperty, deleteProperty } from "lib/resource-properties"; +import { GroupClass } from "models/group"; export interface ProjectCreateFormDialogData { ownerUuid: string; @@ -47,7 +53,12 @@ export const openProjectCreateDialog = (ownerUuid: string) => } else { dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid })); } - dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_CREATE_FORM_NAME, data: {} })); + dispatch(dialogActions.OPEN_DIALOG({ + id: PROJECT_CREATE_FORM_NAME, + data: { + sourcePanel: GroupClass.PROJECT, + } + })); }; export const createProject = (project: Partial) => @@ -66,23 +77,3 @@ export const createProject = (project: Partial) => return undefined; } }; - -export const addPropertyToCreateProjectForm = (data: ResourcePropertiesFormData) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') }; - const key = data.keyID || data.key; - const value = data.valueID || data.value; - dispatch(change( - PROJECT_CREATE_FORM_NAME, - 'properties', - addProperty(properties, key, value))); - }; - -export const removePropertyFromCreateProjectForm = (key: string, value: string) => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') }; - dispatch(change( - PROJECT_CREATE_FORM_NAME, - 'properties', - deleteProperty(properties, key, value))); - }; diff --git a/src/store/projects/project-update-actions.ts b/src/store/projects/project-update-actions.ts index ba176753..52abfd3f 100644 --- a/src/store/projects/project-update-actions.ts +++ b/src/store/projects/project-update-actions.ts @@ -3,28 +3,47 @@ // SPDX-License-Identifier: AGPL-3.0 import { Dispatch } from "redux"; -import { FormErrors, initialize, reset, startSubmit, stopSubmit } from 'redux-form'; +import { + 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 { 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) => { dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource)); - dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {sourcePanel: GroupClass.PROJECT} })); + dispatch(dialogActions.OPEN_DIALOG({ + id: PROJECT_UPDATE_FORM_NAME, + data: { + sourcePanel: GroupClass.PROJECT, + } + })); }; export const updateProject = (project: ProjectUpdateFormDialogData) => @@ -32,7 +51,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 })); diff --git a/src/store/resources/resources-actions.ts b/src/store/resources/resources-actions.ts index 6c05da32..1d1355a8 100644 --- a/src/store/resources/resources-actions.ts +++ b/src/store/resources/resources-actions.ts @@ -3,11 +3,17 @@ // SPDX-License-Identifier: AGPL-3.0 import { unionize, ofType, UnionOf } from 'common/unionize'; -import { extractUuidKind, Resource } from 'models/resource'; +import { extractUuidKind, Resource, ResourceWithProperties } from 'models/resource'; import { Dispatch } from 'redux'; import { RootState } from 'store/store'; import { ServiceRepository } from 'services/services'; import { getResourceService } from 'services/services'; +import { addProperty, deleteProperty } from 'lib/resource-properties'; +import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions'; +import { getResource } from './resources'; +import { TagProperty } from 'models/tag'; +import { change, formValueSelector } from 'redux-form'; +import { ResourcePropertiesFormData } from 'views-components/resource-properties-form/resource-properties-form'; export const resourcesActions = unionize({ SET_RESOURCES: ofType(), @@ -33,3 +39,79 @@ export const loadResource = (uuid: string, showErrors?: boolean) => } catch {} return undefined; }; + +export const deleteResourceProperty = (uuid: string, key: string, value: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { resources } = getState(); + + const rsc = getResource(uuid)(resources) as ResourceWithProperties; + if (!rsc) { return; } + + const kind = extractUuidKind(uuid); + const service = getResourceService(kind)(services); + if (!service) { return; } + + const properties = Object.assign({}, rsc.properties); + + try { + let updatedRsc = await service.update( + uuid, { + properties: deleteProperty(properties, key, value), + }); + updatedRsc = {...rsc, ...updatedRsc}; + dispatch(updateResources([updatedRsc])); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.errors[0], hideDuration: 2000, kind: SnackbarKind.ERROR })); + } + }; + +export const createResourceProperty = (data: TagProperty) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { uuid } = data; + const { resources } = getState(); + + const rsc = getResource(uuid)(resources) as ResourceWithProperties; + if (!rsc) { return; } + + const kind = extractUuidKind(uuid); + const service = getResourceService(kind)(services); + if (!service) { return; } + + try { + const key = data.keyID || data.key; + const value = data.valueID || data.value; + const properties = Object.assign({}, rsc.properties); + let updatedRsc = await service.update( + rsc.uuid, { + properties: addProperty(properties, key, value), + } + ); + updatedRsc = {...rsc, ...updatedRsc}; + dispatch(updateResources([updatedRsc])); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000, kind: SnackbarKind.SUCCESS })); + } catch (e) { + const errorMsg = e.errors && e.errors.length > 0 ? e.errors[0] : "Error while adding property"; + dispatch(snackbarActions.OPEN_SNACKBAR({ message: errorMsg, hideDuration: 2000, kind: SnackbarKind.ERROR })); + } + }; + +export const addPropertyToResourceForm = (data: ResourcePropertiesFormData, formName: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const properties = { ...formValueSelector(formName)(getState(), 'properties') }; + const key = data.keyID || data.key; + const value = data.valueID || data.value; + dispatch(change( + formName, + 'properties', + addProperty(properties, key, value))); + }; + +export const removePropertyFromResourceForm = (key: string, value: string, formName: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const properties = { ...formValueSelector(formName)(getState(), 'properties') }; + dispatch(change( + formName, + '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 index 00000000..3f19e158 --- /dev/null +++ b/src/views-components/collection-properties/create-collection-properties-form.tsx @@ -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, + COLLECTION_CREATE_FORM_NAME +} from 'store/collections/collection-create-actions'; +import { + ResourcePropertiesForm, + ResourcePropertiesFormData +} from 'views-components/resource-properties-form/resource-properties-form'; +import { addPropertyToResourceForm } from 'store/resources/resources-actions'; + +const Form = withStyles( + ({ spacing }) => ( + { container: + { + margin: 0, + } + }) + )(ResourcePropertiesForm); + +export const CreateCollectionPropertiesForm = reduxForm({ + form: COLLECTION_CREATE_PROPERTIES_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(addPropertyToResourceForm(data, COLLECTION_CREATE_FORM_NAME)); + dispatch(reset(COLLECTION_CREATE_PROPERTIES_FORM_NAME)); + } +})(Form); \ 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 index 00000000..9092c7cc --- /dev/null +++ b/src/views-components/collection-properties/update-collection-properties-form.tsx @@ -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_UPDATE_FORM_NAME, + COLLECTION_UPDATE_PROPERTIES_FORM_NAME +} from 'store/collections/collection-update-actions'; +import { + ResourcePropertiesForm, + ResourcePropertiesFormData +} from 'views-components/resource-properties-form/resource-properties-form'; +import { addPropertyToResourceForm } from 'store/resources/resources-actions'; + +const Form = withStyles( + ({ spacing }) => ( + { container: + { + margin: 0, + } + }) + )(ResourcePropertiesForm); + +export const UpdateCollectionPropertiesForm = reduxForm({ + form: COLLECTION_UPDATE_PROPERTIES_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(addPropertyToResourceForm(data, COLLECTION_UPDATE_FORM_NAME)); + dispatch(reset(COLLECTION_UPDATE_PROPERTIES_FORM_NAME)); + } +})(Form); \ No newline at end of file diff --git a/src/views-components/context-menu/action-sets/collection-files-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-action-set.ts index 59a5f368..f34f2868 100644 --- a/src/views-components/context-menu/action-sets/collection-files-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-files-action-set.ts @@ -4,7 +4,11 @@ import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set"; import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "store/collection-panel/collection-panel-files/collection-panel-files-actions"; -import { openCollectionPartialCopyDialog, openCollectionPartialCopyToSelectedCollectionDialog } from 'store/collections/collection-partial-copy-actions'; +import { + openCollectionPartialCopyDialog, + // Disabled while addressing #18587 + // openCollectionPartialCopyToSelectedCollectionDialog +} from 'store/collections/collection-partial-copy-actions'; // These action sets are used on the multi-select actions button. export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[ diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx index dcd2ee48..f2b599e7 100644 --- a/src/views-components/details-panel/collection-details.tsx +++ b/src/views-components/details-panel/collection-details.tsx @@ -3,21 +3,22 @@ // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; -import { CollectionIcon } from 'components/icon/icon'; +import { CollectionIcon, RenameIcon } from 'components/icon/icon'; import { CollectionResource } from 'models/collection'; import { DetailsData } from "./details-data"; import { CollectionDetailsAttributes } from 'views/collection-panel/collection-panel'; import { RootState } from 'store/store'; import { filterResources, getResource } from 'store/resources/resources'; import { connect } from 'react-redux'; -import { Grid, ListItem, StyleRulesCallback, Typography, withStyles, WithStyles } from '@material-ui/core'; +import { Button, Grid, ListItem, StyleRulesCallback, Typography, withStyles, WithStyles } from '@material-ui/core'; import { formatDate, formatFileSize } from 'common/formatters'; import { UserNameFromID } from '../data-explorer/renderers'; import { Dispatch } from 'redux'; import { navigateTo } from 'store/navigation/navigation-action'; import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions'; +import { openCollectionUpdateDialog } from 'store/collections/collection-update-actions'; -export type CssRules = 'versionBrowserHeader' | 'versionBrowserItem' | 'versionBrowserField'; +export type CssRules = 'versionBrowserHeader' | 'versionBrowserItem' | 'versionBrowserField' | 'editIcon'; const styles: StyleRulesCallback = theme => ({ versionBrowserHeader: { @@ -29,7 +30,11 @@ const styles: StyleRulesCallback = theme => ({ }, versionBrowserField: { textAlign: 'center', - } + }, + editIcon: { + paddingRight: theme.spacing.unit/2, + fontSize: '1.125rem', + }, }); export class CollectionDetails extends DetailsData { @@ -54,7 +59,7 @@ export class CollectionDetails extends DetailsData { } private getCollectionInfo() { - return ; + return ; } private getVersionBrowser() { @@ -62,6 +67,47 @@ export class CollectionDetails extends DetailsData { } } +interface CollectionInfoDataProps { + currentCollection: CollectionResource | undefined; +} + +interface CollectionInfoDispatchProps { + editCollection: (collection: CollectionResource | undefined) => void; +} + +const ciMapStateToProps = (state: RootState): CollectionInfoDataProps => { + return { + currentCollection: getResource(state.detailsPanel.resourceUuid)(state.resources), + }; +}; + +const ciMapDispatchToProps = (dispatch: Dispatch): CollectionInfoDispatchProps => ({ + editCollection: (collection: CollectionResource) => + dispatch(openCollectionUpdateDialog({ + uuid: collection.uuid, + name: collection.name, + description: collection.description, + properties: collection.properties, + storageClassesDesired: collection.storageClassesDesired, + })), +}); + +type CollectionInfoProps = CollectionInfoDataProps & CollectionInfoDispatchProps & WithStyles; + +const CollectionInfo = withStyles(styles)( + connect(ciMapStateToProps, ciMapDispatchToProps)( + ({ currentCollection, editCollection, classes }: CollectionInfoProps) => + currentCollection !== undefined + ?
+ + +
+ :
+ ) +); + interface CollectionVersionBrowserProps { currentCollection: CollectionResource | undefined; versions: CollectionResource[]; @@ -72,7 +118,7 @@ interface CollectionVersionBrowserDispatchProps { handleContextMenu: (event: React.MouseEvent, collection: CollectionResource) => void; } -const mapStateToProps = (state: RootState): CollectionVersionBrowserProps => { +const vbMapStateToProps = (state: RootState): CollectionVersionBrowserProps => { const currentCollection = getResource(state.detailsPanel.resourceUuid)(state.resources); const versions = (currentCollection && filterResources(rsc => @@ -82,7 +128,7 @@ const mapStateToProps = (state: RootState): CollectionVersionBrowserProps => { return { currentCollection, versions }; }; -const mapDispatchToProps = () => +const vbMapDispatchToProps = () => (dispatch: Dispatch): CollectionVersionBrowserDispatchProps => ({ showVersion: (collection) => dispatch(navigateTo(collection.uuid)), handleContextMenu: (event: React.MouseEvent, collection: CollectionResource) => { @@ -103,7 +149,7 @@ const mapDispatchToProps = () => }); const CollectionVersionBrowser = withStyles(styles)( - connect(mapStateToProps, mapDispatchToProps)( + connect(vbMapStateToProps, vbMapDispatchToProps)( ({ currentCollection, versions, showVersion, handleContextMenu, classes }: CollectionVersionBrowserProps & CollectionVersionBrowserDispatchProps & WithStyles) => { return
diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx index 058db81b..399f4ef4 100644 --- a/src/views-components/details-panel/details-panel.tsx +++ b/src/views-components/details-panel/details-panel.tsx @@ -160,6 +160,7 @@ export const DetailsPanel = withStyles(styles)( const item = getItem(res); return { getIcon(className?: string) { @@ -41,8 +41,8 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ marginBottom: theme.spacing.unit }, editIcon: { + paddingRight: theme.spacing.unit/2, fontSize: '1.125rem', - cursor: 'pointer' } }); @@ -51,11 +51,12 @@ interface ProjectDetailsComponentDataProps { } interface ProjectDetailsComponentActionProps { - onClick: () => void; + onClick: (prj: ProjectUpdateFormDialogData) => () => void; } const mapDispatchToProps = (dispatch: Dispatch) => ({ - onClick: () => dispatch(openProjectPropertiesDialog()), + onClick: (prj: ProjectUpdateFormDialogData) => + () => dispatch(openProjectUpdateDialog(prj)), }); type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles; @@ -63,6 +64,17 @@ type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDe const ProjectDetailsComponent = connect(null, mapDispatchToProps)( withStyles(styles)( ({ classes, project, onClick }: ProjectDetailsComponentProps) =>
+ {project.groupClass !== GroupClass.FILTER ? + + : '' + } } /> @@ -78,14 +90,7 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)( : '---' } - - {project.groupClass !== GroupClass.FILTER ? -
- -
- : '' - } -
+ { Object.keys(project.properties).map(k => Array.isArray(project.properties[k]) diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx index c85a6d12..17a24e48 100644 --- a/src/views-components/dialog-create/dialog-collection-create.tsx +++ b/src/views-components/dialog-create/dialog-collection-create.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { InjectedFormProps, Field } from 'redux-form'; import { WithDialogProps } from 'store/dialog/with-dialog'; -import { CollectionCreateFormDialogData } from 'store/collections/collection-create-actions'; +import { CollectionCreateFormDialogData, COLLECTION_CREATE_FORM_NAME } from 'store/collections/collection-create-actions'; import { FormDialog } from 'components/form-dialog/form-dialog'; import { CollectionNameField, @@ -14,25 +14,47 @@ 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 { CreateCollectionPropertiesForm } from 'views-components/collection-properties/create-collection-properties-form'; +import { FormGroup, FormLabel, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { resourcePropertiesList } from 'views-components/resource-properties/resource-properties-list'; + +type CssRules = 'propertiesForm'; + +const styles: StyleRulesCallback = theme => ({ + propertiesForm: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, +}); type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps; export const DialogCollectionCreate = (props: DialogCollectionProps) => ; -const CollectionAddFields = () => - - - - - -; +const CreateCollectionPropertiesList = resourcePropertiesList(COLLECTION_CREATE_FORM_NAME); + +const CollectionAddFields = withStyles(styles)( + ({ classes }: WithStyles) => + + + +
+ Properties + + + + +
+ + +
); diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx index 85a2380e..d85a304e 100644 --- a/src/views-components/dialog-create/dialog-project-create.tsx +++ b/src/views-components/dialog-create/dialog-project-create.tsx @@ -5,27 +5,78 @@ import React from 'react'; import { InjectedFormProps } from 'redux-form'; import { WithDialogProps } from 'store/dialog/with-dialog'; -import { ProjectCreateFormDialogData } from 'store/projects/project-create-actions'; +import { ProjectCreateFormDialogData, PROJECT_CREATE_FORM_NAME } from 'store/projects/project-create-actions'; import { FormDialog } from 'components/form-dialog/form-dialog'; -import { ProjectNameField, ProjectDescriptionField } from 'views-components/form-fields/project-form-fields'; +import { ProjectNameField, ProjectDescriptionField, UsersField } from 'views-components/form-fields/project-form-fields'; 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, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { resourcePropertiesList } from 'views-components/resource-properties/resource-properties-list'; +import { GroupClass } from 'models/group'; -type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps; +type CssRules = 'propertiesForm' | 'description'; -export const DialogProjectCreate = (props: DialogProjectProps) => - = theme => ({ + propertiesForm: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, + description: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, +}); + +type DialogProjectProps = WithDialogProps<{sourcePanel: GroupClass}> & InjectedFormProps; + +export const DialogProjectCreate = (props: DialogProjectProps) => { + let title = 'New Project'; + let fields = ProjectAddFields; + const sourcePanel = props.data.sourcePanel || ''; + + if (sourcePanel === GroupClass.ROLE) { + title = 'New Group'; + fields = GroupAddFields; + } + + return ; +}; + +const CreateProjectPropertiesList = resourcePropertiesList(PROJECT_CREATE_FORM_NAME); + +const ProjectAddFields = withStyles(styles)( + ({ classes }: WithStyles) => + + +
+ +
+
+ Properties + + + + +
+
); -const ProjectAddFields = () => - - - - - -; +const GroupAddFields = withStyles(styles)( + ({ classes }: WithStyles) => + + +
+ +
+
+ Properties + + + + +
+
); diff --git a/src/views-components/dialog-forms/create-collection-dialog.ts b/src/views-components/dialog-forms/create-collection-dialog.ts index 7ef6e4b3..d989d431 100644 --- a/src/views-components/dialog-forms/create-collection-dialog.ts +++ b/src/views-components/dialog-forms/create-collection-dialog.ts @@ -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); diff --git a/src/views-components/dialog-forms/create-project-dialog.ts b/src/views-components/dialog-forms/create-project-dialog.ts index c0ece675..5c30281f 100644 --- a/src/views-components/dialog-forms/create-project-dialog.ts +++ b/src/views-components/dialog-forms/create-project-dialog.ts @@ -8,13 +8,24 @@ import { withDialog } from "store/dialog/with-dialog"; import { PROJECT_CREATE_FORM_NAME, ProjectCreateFormDialogData } from 'store/projects/project-create-actions'; import { DialogProjectCreate } from 'views-components/dialog-create/dialog-project-create'; import { createProject } from "store/workbench/workbench-actions"; +import { GroupClass } from "models/group"; +import { createGroup } from "store/groups-panel/groups-panel-actions"; export const CreateProjectDialog = compose( withDialog(PROJECT_CREATE_FORM_NAME), reduxForm({ form: PROJECT_CREATE_FORM_NAME, - onSubmit: (data, dispatch) => { - dispatch(createProject(data)); + onSubmit: (data, dispatch, props) => { + switch (props.data.sourcePanel) { + case GroupClass.PROJECT: + dispatch(createProject(data)); + break; + case GroupClass.ROLE: + dispatch(createGroup(data)); + break; + default: + break; + } } }) )(DialogProjectCreate); \ No newline at end of file diff --git a/src/views-components/dialog-forms/update-project-dialog.ts b/src/views-components/dialog-forms/update-project-dialog.ts index 4ba03f2f..94620904 100644 --- a/src/views-components/dialog-forms/update-project-dialog.ts +++ b/src/views-components/dialog-forms/update-project-dialog.ts @@ -9,7 +9,6 @@ import { DialogProjectUpdate } from 'views-components/dialog-update/dialog-proje import { PROJECT_UPDATE_FORM_NAME, ProjectUpdateFormDialogData } from 'store/projects/project-update-actions'; import { updateProject, updateGroup } from 'store/workbench/workbench-actions'; import { GroupClass } from "models/group"; -import { createGroup } from "store/groups-panel/groups-panel-actions"; export const UpdateProjectDialog = compose( withDialog(PROJECT_UPDATE_FORM_NAME), @@ -21,11 +20,7 @@ export const UpdateProjectDialog = compose( dispatch(updateProject(data)); break; case GroupClass.ROLE: - if (data.uuid) { - dispatch(updateGroup(data)); - } else { - dispatch(createGroup(data)); - } + dispatch(updateGroup(data)); break; default: break; diff --git a/src/views-components/dialog-update/dialog-collection-update.tsx b/src/views-components/dialog-update/dialog-collection-update.tsx index cce64d27..d77d10ff 100644 --- a/src/views-components/dialog-update/dialog-collection-update.tsx +++ b/src/views-components/dialog-update/dialog-collection-update.tsx @@ -5,26 +5,48 @@ import React from 'react'; import { InjectedFormProps } from 'redux-form'; import { WithDialogProps } from 'store/dialog/with-dialog'; -import { CollectionUpdateFormDialogData } from 'store/collections/collection-update-actions'; +import { CollectionUpdateFormDialogData, COLLECTION_UPDATE_FORM_NAME } from 'store/collections/collection-update-actions'; import { FormDialog } from 'components/form-dialog/form-dialog'; import { CollectionNameField, CollectionDescriptionField, CollectionStorageClassesField } from 'views-components/form-fields/collection-form-fields'; +import { UpdateCollectionPropertiesForm } from 'views-components/collection-properties/update-collection-properties-form'; +import { FormGroup, FormLabel, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core'; +import { resourcePropertiesList } from 'views-components/resource-properties/resource-properties-list'; + +type CssRules = 'propertiesForm'; + +const styles: StyleRulesCallback = theme => ({ + propertiesForm: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, +}); type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps; export const DialogCollectionUpdate = (props: DialogCollectionProps) => ; -const CollectionEditFields = () => - - - -; +const UpdateCollectionPropertiesList = resourcePropertiesList(COLLECTION_UPDATE_FORM_NAME); + +const CollectionEditFields = withStyles(styles)( + ({ classes }: WithStyles) => + + +
+ Properties + + + + +
+ +
); diff --git a/src/views-components/dialog-update/dialog-project-update.tsx b/src/views-components/dialog-update/dialog-project-update.tsx index fda7c47d..a6ac65b1 100644 --- a/src/views-components/dialog-update/dialog-project-update.tsx +++ b/src/views-components/dialog-update/dialog-project-update.tsx @@ -5,40 +5,59 @@ import React from 'react'; import { InjectedFormProps } from 'redux-form'; import { WithDialogProps } from 'store/dialog/with-dialog'; -import { ProjectUpdateFormDialogData } from 'store/projects/project-update-actions'; +import { ProjectUpdateFormDialogData, PROJECT_UPDATE_FORM_NAME } from 'store/projects/project-update-actions'; import { FormDialog } from 'components/form-dialog/form-dialog'; -import { ProjectNameField, ProjectDescriptionField, UsersField } from 'views-components/form-fields/project-form-fields'; +import { ProjectNameField, ProjectDescriptionField } from 'views-components/form-fields/project-form-fields'; import { GroupClass } from 'models/group'; +import { FormGroup, FormLabel, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { UpdateProjectPropertiesForm } from 'views-components/project-properties/update-project-properties-form'; +import { resourcePropertiesList } from 'views-components/resource-properties/resource-properties-list'; -type DialogProjectProps = WithDialogProps<{sourcePanel: GroupClass, create?: boolean}> & InjectedFormProps; +type CssRules = 'propertiesForm' | 'description'; + +const styles: StyleRulesCallback = theme => ({ + propertiesForm: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, + description: { + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, + }, +}); + +type DialogProjectProps = WithDialogProps<{sourcePanel: GroupClass}> & InjectedFormProps; export const DialogProjectUpdate = (props: DialogProjectProps) => { let title = 'Edit Project'; - let fields = ProjectEditFields; const sourcePanel = props.data.sourcePanel || ''; - const create = !!props.data.create; if (sourcePanel === GroupClass.ROLE) { - title = create ? 'Create Group' : 'Edit Group'; - fields = create ? GroupAddFields : ProjectEditFields; + title = 'Edit Group'; } return ; }; +const UpdateProjectPropertiesList = resourcePropertiesList(PROJECT_UPDATE_FORM_NAME); + // Also used as "Group Edit Fields" -const ProjectEditFields = () => - - -; - -const GroupAddFields = () => - - - -; +const ProjectEditFields = withStyles(styles)( + ({ classes }: WithStyles) => + +
+ +
+
+ Properties + + + + +
+
); diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx deleted file mode 100644 index 19d3bb56..00000000 --- a/src/views-components/project-properties-dialog/project-properties-dialog.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import 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, 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'; -import { getPropertyChip } from "../resource-properties-form/property-chip"; - -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, value: string) => void; -} - -const mapStateToProps = ({ detailsPanel, resources, properties }: RootState): ProjectPropertiesDialogDataProps => ({ - project: getResource(detailsPanel.resourceUuid)(resources) as ProjectResource, -}); - -const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({ - handleDelete: (key: string, value: string) => () => dispatch(deleteProjectProperty(key, value)), -}); - -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 => - 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/project-properties-dialog/project-properties-form.tsx b/src/views-components/project-properties-dialog/project-properties-form.tsx deleted file mode 100644 index f36bacf4..00000000 --- a/src/views-components/project-properties-dialog/project-properties-form.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { reduxForm, reset } from 'redux-form'; -import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from 'store/details-panel/details-panel-action'; -import { ResourcePropertiesForm, ResourcePropertiesFormData } from 'views-components/resource-properties-form/resource-properties-form'; -import { withStyles } from '@material-ui/core'; -import { Dispatch } from 'redux'; - -const Form = withStyles(({ spacing }) => ({ container: { marginBottom: spacing.unit * 2 } }))(ResourcePropertiesForm); - -export const ProjectPropertiesForm = reduxForm({ - form: PROJECT_PROPERTIES_FORM_NAME, - onSubmit: (data, dispatch: Dispatch) => { - dispatch(createProjectProperty(data)); - dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); - } -})(Form); diff --git a/src/views-components/project-properties/create-project-properties-form.tsx b/src/views-components/project-properties/create-project-properties-form.tsx index c49d738a..8c26523e 100644 --- a/src/views-components/project-properties/create-project-properties-form.tsx +++ b/src/views-components/project-properties/create-project-properties-form.tsx @@ -6,18 +6,18 @@ import { reduxForm, reset } from 'redux-form'; import { withStyles } from '@material-ui/core'; import { PROJECT_CREATE_PROPERTIES_FORM_NAME, - addPropertyToCreateProjectForm + PROJECT_CREATE_FORM_NAME } from 'store/projects/project-create-actions'; import { ResourcePropertiesForm, ResourcePropertiesFormData } from 'views-components/resource-properties-form/resource-properties-form'; +import { addPropertyToResourceForm } from 'store/resources/resources-actions'; const Form = withStyles( ({ spacing }) => ( { container: { - paddingTop: spacing.unit, margin: 0, } }) @@ -26,7 +26,7 @@ const Form = withStyles( export const CreateProjectPropertiesForm = reduxForm({ form: PROJECT_CREATE_PROPERTIES_FORM_NAME, onSubmit: (data, dispatch) => { - dispatch(addPropertyToCreateProjectForm(data)); + dispatch(addPropertyToResourceForm(data, PROJECT_CREATE_FORM_NAME)); dispatch(reset(PROJECT_CREATE_PROPERTIES_FORM_NAME)); } })(Form); \ No newline at end of file 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 index 00000000..0b5554bc --- /dev/null +++ b/src/views-components/project-properties/update-project-properties-form.tsx @@ -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, + PROJECT_UPDATE_FORM_NAME +} from 'store/projects/project-update-actions'; +import { + ResourcePropertiesForm, + ResourcePropertiesFormData +} from 'views-components/resource-properties-form/resource-properties-form'; +import { addPropertyToResourceForm } from 'store/resources/resources-actions'; + +const Form = withStyles( + ({ spacing }) => ( + { container: + { + margin: 0, + } + }) + )(ResourcePropertiesForm); + +export const UpdateProjectPropertiesForm = reduxForm({ + form: PROJECT_UPDATE_PROPERTIES_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(addPropertyToResourceForm(data, PROJECT_UPDATE_FORM_NAME)); + dispatch(reset(PROJECT_UPDATE_PROPERTIES_FORM_NAME)); + } +})(Form); \ No newline at end of file diff --git a/src/views-components/resource-properties-form/resource-properties-form.tsx b/src/views-components/resource-properties-form/resource-properties-form.tsx index 38d76e46..979d772e 100644 --- a/src/views-components/resource-properties-form/resource-properties-form.tsx +++ b/src/views-components/resource-properties-form/resource-properties-form.tsx @@ -11,16 +11,18 @@ import { ProgressButton } from 'components/progress-button/progress-button'; import { GridClassKey } from '@material-ui/core/Grid'; export interface ResourcePropertiesFormData { + uuid: string; [PROPERTY_KEY_FIELD_NAME]: string; [PROPERTY_KEY_FIELD_ID]: string; [PROPERTY_VALUE_FIELD_NAME]: string; [PROPERTY_VALUE_FIELD_ID]: string; } -export type ResourcePropertiesFormProps = InjectedFormProps & WithStyles; +export type ResourcePropertiesFormProps = {uuid: string; } & InjectedFormProps & WithStyles; -export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) => -
+export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, invalid, classes, uuid }: ResourcePropertiesFormProps ) => { + change('uuid', uuid); // Sets the uuid field to the uuid of the resource. + return @@ -28,7 +30,7 @@ export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, clas - +