[arvados-workbench2] created: 2.4.1-5-g41f1383f

git repository hosting git at public.arvados.org
Wed Aug 3 20:31:33 UTC 2022


        at  41f1383f355af8340d789f4b364d6eb72de4497d (commit)


commit 41f1383f355af8340d789f4b364d6eb72de4497d
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Aug 2 11:46:53 2022 -0400

    Merge branch '19305-project-update-dialog-properties' into main. Closes #19305
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index c4983e3e..b2f6f33d 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -85,6 +85,39 @@ describe('Project tests', function() {
                 // Pink is not in the test vocab
                 {IDTAGCOLORS: ['IDVALCOLORS3', 'Pink', 'IDVALCOLORS1']});
         });
+
+        // Open project edit via breadcrumbs
+        cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+        cy.get('[data-cy=context-menu]').contains('Edit').click();
+        cy.get('[data-cy=form-dialog]').within(() => {
+            cy.get('[data-cy=resource-properties-list]').within(() => {
+                cy.get('div[role=button]').contains('Color: Magenta');
+                cy.get('div[role=button]').contains('Color: Pink');
+                cy.get('div[role=button]').contains('Color: Yellow');
+            });
+        });
+        // Add another property
+        cy.get('[data-cy=resource-properties-form]').within(() => {
+            cy.get('[data-cy=property-field-key]').within(() => {
+                cy.get('input').type('Animal');
+            });
+            cy.get('[data-cy=property-field-value]').within(() => {
+                cy.get('input').type('Dog');
+            });
+            cy.root().submit();
+        });
+        cy.get('[data-cy=form-submit-btn]').click();
+        // Reopen edit via breadcrumbs and verify properties
+        cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+        cy.get('[data-cy=context-menu]').contains('Edit').click();
+        cy.get('[data-cy=form-dialog]').within(() => {
+            cy.get('[data-cy=resource-properties-list]').within(() => {
+                cy.get('div[role=button]').contains('Color: Magenta');
+                cy.get('div[role=button]').contains('Color: Pink');
+                cy.get('div[role=button]').contains('Color: Yellow');
+                cy.get('div[role=button]').contains('Animal: Dog');
+            });
+        });
     });
 
     it('creates new project on home project and then a subproject inside it', function() {
diff --git a/src/store/projects/project-update-actions.ts b/src/store/projects/project-update-actions.ts
index 52abfd3f..a6e67485 100644
--- a/src/store/projects/project-update-actions.ts
+++ b/src/store/projects/project-update-actions.ts
@@ -22,6 +22,8 @@ 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";
+import { getResource } from "store/resources/resources";
+import { ProjectResource } from "models/project";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -37,7 +39,9 @@ export const PROJECT_UPDATE_FORM_SELECTOR = formValueSelector(PROJECT_UPDATE_FOR
 
 export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
+        // Get complete project resource from store to handle consumers passing in partial resources
+        const project = getResource<ProjectResource>(resource.uuid)(getState().resources);
+        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
         dispatch(dialogActions.OPEN_DIALOG({
             id: PROJECT_UPDATE_FORM_NAME,
             data: {
diff --git a/src/views-components/resource-properties/resource-properties-list.tsx b/src/views-components/resource-properties/resource-properties-list.tsx
index a7b58252..47d7729b 100644
--- a/src/views-components/resource-properties/resource-properties-list.tsx
+++ b/src/views-components/resource-properties/resource-properties-list.tsx
@@ -38,7 +38,7 @@ ResourcePropertiesListActionProps & WithStyles<CssRules>;
 
 const List = withStyles(styles)(
     ({ classes, handleDelete, properties }: ResourcePropertiesListProps) =>
-        <div>
+        <div data-cy="resource-properties-list">
             {properties &&
                 Object.keys(properties).map(k =>
                     Array.isArray(properties[k])
@@ -63,4 +63,4 @@ export const resourcePropertiesList = (formName: string) =>
         (dispatch: Dispatch): ResourcePropertiesListActionProps => ({
                 handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromResourceForm(key, value, formName))
         })
-    )(List);
\ No newline at end of file
+    )(List);

commit f0e2e73a18bff1e73c799f2da16bbc66210edf12
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Jul 22 16:51:25 2022 -0400

    Merge branch '18965-login-flow-destination' into main. Closes #18965
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index 548dbcaa..52bfa29e 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -68,13 +68,16 @@ export class AuthService {
         }
     }
 
+    public setTargetUrl(url: string) {
+        localStorage.setItem(TARGET_URL, url);
+    }
+
     public removeTargetURL() {
         localStorage.removeItem(TARGET_URL);
-        sessionStorage.removeItem(TARGET_URL);
     }
 
     public getTargetURL() {
-        return this.getStorage().getItem(TARGET_URL);
+        return localStorage.getItem(TARGET_URL);
     }
 
     public removeApiToken() {
@@ -113,7 +116,7 @@ export class AuthService {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
         const homeClusterHost = remoteHosts[homeCluster];
         const rd = new URL(window.location.href);
-        this.getStorage().setItem(TARGET_URL, rd.pathname + rd.search);
+        this.setTargetUrl(rd.pathname + rd.search);
         window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
     }
 
diff --git a/src/views-components/login-form/login-form.tsx b/src/views-components/login-form/login-form.tsx
index aac13642..3aa9e3f2 100644
--- a/src/views-components/login-form/login-form.tsx
+++ b/src/views-components/login-form/login-form.tsx
@@ -12,6 +12,7 @@ import { AxiosPromise } from 'axios';
 import { DispatchProp } from 'react-redux';
 import { saveApiToken } from 'store/auth/auth-action';
 import { navigateToRootProject } from 'store/navigation/navigation-action';
+import { replace } from 'react-router-redux';
 
 type CssRules = 'root' | 'loginBtn' | 'card' | 'wrapper' | 'progress';
 
@@ -87,8 +88,11 @@ export const LoginForm = withStyles(styles)(
                 setSubmitting(false);
                 if (response.data.uuid && response.data.api_token) {
                     const apiToken = `v2/${response.data.uuid}/${response.data.api_token}`;
+                    const rd = new URL(window.location.href);
+                    const rdUrl = rd.pathname + rd.search;
                     dispatch<any>(saveApiToken(apiToken)).finally(
-                        () => dispatch(navigateToRootProject));
+                        () => rdUrl === '/' ? dispatch(navigateToRootProject) : dispatch(replace(rdUrl))
+                    );
                 } else {
                     setError(true);
                     setHelperText(response.data.message || 'Please try again');

commit 955e8da986be952c0d29e46f7305af64f95dffef
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jun 22 15:55:18 2022 -0400

    Merge branch '19153-sharing-links-inline' into main. Closes #19153
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index 7371ed06..c4983e3e 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -82,7 +82,8 @@ describe('Project tests', function() {
         .then(function() {
             expect(this.projects).to.have.lengthOf(1);
             expect(this.projects[0].properties).to.deep.equal(
-                {IDTAGCOLORS: 'IDVALCOLORS3'});
+                // Pink is not in the test vocab
+                {IDTAGCOLORS: ['IDVALCOLORS3', 'Pink', 'IDVALCOLORS1']});
         });
     });
 
@@ -245,7 +246,7 @@ describe('Project tests', function() {
         cy.getAll('@mainProject')
             .then(function ([mainProject]) {
                 cy.loginAs(adminUser);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
 
                 cy.get('[data-cy=uuid]').eq(0).invoke('text').then(uuid => {
@@ -277,4 +278,4 @@ describe('Project tests', function() {
                 });
         });
     });
-});
\ No newline at end of file
+});
diff --git a/src/common/redirect-to.test.ts b/src/common/redirect-to.test.ts
index e25d7be9..0168fd80 100644
--- a/src/common/redirect-to.test.ts
+++ b/src/common/redirect-to.test.ts
@@ -8,7 +8,7 @@ describe('redirect-to', () => {
     const { location } = window;
     const config: any = {
         keepWebServiceUrl: 'http://localhost',
-        keepWebServiceInlineUrl: 'http://localhost'
+        keepWebServiceInlineUrl: 'http://localhost-inline'
     };
     const redirectTo = '/test123';
     const locationTemplate = {
@@ -36,7 +36,7 @@ describe('redirect-to', () => {
             delete window.location;
             window.location = {
                 ...locationTemplate,
-                href: `${location.href}?redirectTo=${redirectTo}`,
+                href: `${location.href}?redirectToDownload=${redirectTo}`,
             } as any;
             Object.defineProperty(window, 'localStorage', {
                 value: {
@@ -51,7 +51,7 @@ describe('redirect-to', () => {
             storeRedirects();
 
             // then
-            expect(window.localStorage.setItem).toHaveBeenCalledWith('redirectTo', redirectTo);
+            expect(window.localStorage.setItem).toHaveBeenCalledWith('redirectToDownload', redirectTo);
         });
     });
 
@@ -60,7 +60,7 @@ describe('redirect-to', () => {
             delete window.location;
             window.location = {
                 ...locationTemplate,
-                href: `${location.href}?redirectTo=${redirectTo}`,
+                href: `${location.href}?redirectToDownload=${redirectTo}`,
             } as any;;
             Object.defineProperty(window, 'localStorage', {
                 value: {
diff --git a/src/common/redirect-to.ts b/src/common/redirect-to.ts
index 77be742f..d8fecde4 100644
--- a/src/common/redirect-to.ts
+++ b/src/common/redirect-to.ts
@@ -2,34 +2,57 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { getInlineFileUrl } from 'views-components/context-menu/actions/helpers';
 import { Config } from './config';
 
-const REDIRECT_TO_KEY = 'redirectTo';
+export const REDIRECT_TO_DOWNLOAD_KEY = 'redirectToDownload';
+export const REDIRECT_TO_PREVIEW_KEY = 'redirectToPreview';
+
+const getRedirectKeyFromUrl = (href: string): string | null => {
+    switch (true) {
+        case href.indexOf(REDIRECT_TO_DOWNLOAD_KEY) > -1:
+            return REDIRECT_TO_DOWNLOAD_KEY;
+        case href.indexOf(REDIRECT_TO_PREVIEW_KEY) > -1:
+            return REDIRECT_TO_PREVIEW_KEY;
+        default:
+            return null;
+    }
+}
+
+const getRedirectKeyFromStorage = (localStorage: Storage): string | null => {
+    if (localStorage.getItem(REDIRECT_TO_DOWNLOAD_KEY)) {
+        return REDIRECT_TO_DOWNLOAD_KEY;
+    } else if (localStorage.getItem(REDIRECT_TO_PREVIEW_KEY)) {
+        return REDIRECT_TO_PREVIEW_KEY;
+    }
+    return null;
+}
 
 export const storeRedirects = () => {
-    let redirectUrl;
     const { location: { href }, localStorage } = window;
+    const redirectKey = getRedirectKeyFromUrl(href);
 
-    if (href.indexOf(REDIRECT_TO_KEY) > -1) {
-        redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
-    }
-
-    if (localStorage && redirectUrl) {
-        localStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
+    if (localStorage && redirectKey) {
+        localStorage.setItem(redirectKey, href.split(`${redirectKey}=`)[1]);
     }
 };
 
 export const handleRedirects = (token: string, config: Config) => {
     const { localStorage } = window;
-    const { keepWebServiceUrl } = config;
-
-    if (localStorage && localStorage.getItem(REDIRECT_TO_KEY)) {
-        const redirectUrl = localStorage.getItem(REDIRECT_TO_KEY);
-        localStorage.removeItem(REDIRECT_TO_KEY);
-
-        if (redirectUrl) {
-            const sep = redirectUrl.indexOf("?") > -1 ? "&" : "?";
-            window.location.href = `${keepWebServiceUrl}${redirectUrl}${sep}api_token=${token}`;
+    const { keepWebServiceUrl, keepWebInlineServiceUrl } = config;
+
+    if (localStorage) {
+        const redirectKey = getRedirectKeyFromStorage(localStorage);
+        const redirectPath = redirectKey ? localStorage.getItem(redirectKey) : '';
+        redirectKey && localStorage.removeItem(redirectKey);
+
+        if (redirectKey && redirectPath) {
+            const sep = redirectPath.indexOf("?") > -1 ? "&" : "?";
+            let redirectUrl = `${keepWebServiceUrl}${redirectPath}${sep}api_token=${token}`;
+            if (redirectKey === REDIRECT_TO_PREVIEW_KEY) {
+                redirectUrl = getInlineFileUrl(redirectUrl, keepWebServiceUrl, keepWebInlineServiceUrl);
+            }
+            window.location.href = redirectUrl;
         }
     }
 };
diff --git a/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx b/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
index a1dc5950..c3408740 100644
--- a/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
+++ b/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
@@ -11,7 +11,7 @@ import { getClipboardUrl } from "./helpers";
 export const CopyToClipboardAction = (props: { href?: any, download?: any, onClick?: () => void, kind?: string, currentCollectionUuid?: string; }) => {
     const copyToClipboard = () => {
         if (props.href) {
-            const clipboardUrl = getClipboardUrl(props.href);
+            const clipboardUrl = getClipboardUrl(props.href, true, true);
             copy(clipboardUrl);
         }
 
@@ -30,4 +30,4 @@ export const CopyToClipboardAction = (props: { href?: any, download?: any, onCli
             </ListItemText>
         </ListItem>
         : null;
-};
\ No newline at end of file
+};
diff --git a/src/views-components/context-menu/actions/helpers.test.ts b/src/views-components/context-menu/actions/helpers.test.ts
index b2cb4a55..b3b7f7f8 100644
--- a/src/views-components/context-menu/actions/helpers.test.ts
+++ b/src/views-components/context-menu/actions/helpers.test.ts
@@ -25,7 +25,7 @@ describe('helpers', () => {
             const result = getClipboardUrl(url);
 
             // then
-            expect(result).toBe('http://localhost?redirectTo=https://example.com/c=zzzzz-4zz18-0123456789abcde/LIMS/1.html');
+            expect(result).toBe('http://localhost?redirectToDownload=https://example.com/c=zzzzz-4zz18-0123456789abcde/LIMS/1.html');
         });
     });
 
@@ -67,4 +67,4 @@ describe('helpers', () => {
             expect(result).toBe('https://download.example.com/c=zzzzz-4zz18-0123456789abcde/t=v2/a/b/LIMS/1.html');
         });
     });
-});
\ No newline at end of file
+});
diff --git a/src/views-components/context-menu/actions/helpers.ts b/src/views-components/context-menu/actions/helpers.ts
index 159b1c18..f196074d 100644
--- a/src/views-components/context-menu/actions/helpers.ts
+++ b/src/views-components/context-menu/actions/helpers.ts
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { REDIRECT_TO_DOWNLOAD_KEY, REDIRECT_TO_PREVIEW_KEY } from "common/redirect-to";
 import { extractUuidKind, ResourceKind } from "models/resource";
 
 export const sanitizeToken = (href: string, tokenAsQueryParam = true): string => {
@@ -13,11 +14,12 @@ export const sanitizeToken = (href: string, tokenAsQueryParam = true): string =>
     return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
 };
 
-export const getClipboardUrl = (href: string, shouldSanitizeToken = true): string => {
+export const getClipboardUrl = (href: string, shouldSanitizeToken = true, inline = false): string => {
     const { origin } = window.location;
     const url = shouldSanitizeToken ? sanitizeToken(href, false) : href;
+    const redirectKey = inline ? REDIRECT_TO_PREVIEW_KEY : REDIRECT_TO_DOWNLOAD_KEY;
 
-    return shouldSanitizeToken ? `${origin}?redirectTo=${url}` : `${origin}${url}`;
+    return shouldSanitizeToken ? `${origin}?${redirectKey}=${url}` : `${origin}${url}`;
 };
 
 export const getInlineFileUrl = (url: string, keepWebSvcUrl: string, keepWebInlineSvcUrl: string): string => {

commit bc6ed3985124a18d2c104b794ebc5a73ab872af1
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Tue Jun 21 11:16:57 2022 +0200

    Merge branch '18203-Support-setting-multi-properties-at-once' into main
    closes #18203
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index b62a3441..5e8d1fe5 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -419,6 +419,20 @@ describe('Collection panel tests', function () {
             });
     });
 
+    it('shows collection owner', () => {
+        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((testCollection) => {
+                cy.loginAs(activeUser);
+                cy.goToPath(`/collections/${testCollection.uuid}`);
+                cy.wait(5000);
+                cy.get('[data-cy=collection-info-panel]').contains(`Collection User`);
+            });
+    });
+
     it('tries to rename a file with illegal names', function () {
         // Creates the collection using the admin token so we can set up
         // a bogus manifest text without block signatures.
diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index 0017e416..7371ed06 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -28,7 +28,7 @@ describe('Project tests', function() {
         cy.clearLocalStorage();
     });
 
-    it('creates a new project with properties', function() {
+    it('creates a new project with multiple properties', function() {
         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
         cy.loginAs(activeUser);
         cy.get('[data-cy=side-panel-button]').click();
@@ -51,9 +51,26 @@ describe('Project tests', function() {
                 cy.get('input').type('Magenta');
             });
             cy.root().submit();
+            cy.get('[data-cy=property-field-value]').within(() => {
+                cy.get('input').type('Pink');
+            });
+            cy.root().submit();
+            cy.get('[data-cy=property-field-value]').within(() => {
+                cy.get('input').type('Yellow');
+            });
+            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-dialog]').should('contain', 'Color: Pink');
+        cy.get('[data-cy=form-dialog]').should('contain', 'Color: Yellow');
+
+        cy.get('[data-cy=resource-properties-form]').within(() => {
+            cy.get('[data-cy=property-field-key]').within(() => {
+                cy.get('input').focus();
+            });
+            cy.get('[data-cy=property-field-key]').should('not.contain', 'Color');
+        });
 
         // Create project and confirm the properties' real values.
         cy.get('[data-cy=form-submit-btn]').click();
diff --git a/src/views-components/collection-properties/create-collection-properties-form.tsx b/src/views-components/collection-properties/create-collection-properties-form.tsx
index 3f19e158..fb18bb1a 100644
--- a/src/views-components/collection-properties/create-collection-properties-form.tsx
+++ b/src/views-components/collection-properties/create-collection-properties-form.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reduxForm, reset } from 'redux-form';
+import { reduxForm, change } from 'redux-form';
 import { withStyles } from '@material-ui/core';
 import {
     COLLECTION_CREATE_PROPERTIES_FORM_NAME,
@@ -13,6 +13,7 @@ import {
     ResourcePropertiesFormData
 } from 'views-components/resource-properties-form/resource-properties-form';
 import { addPropertyToResourceForm } from 'store/resources/resources-actions';
+import { PROPERTY_VALUE_FIELD_NAME } from 'views-components/resource-properties-form/property-value-field';
 
 const Form = withStyles(
     ({ spacing }) => (
@@ -27,6 +28,6 @@ export const CreateCollectionPropertiesForm = reduxForm<ResourcePropertiesFormDa
     form: COLLECTION_CREATE_PROPERTIES_FORM_NAME,
     onSubmit: (data, dispatch) => {
         dispatch(addPropertyToResourceForm(data, COLLECTION_CREATE_FORM_NAME));
-        dispatch(reset(COLLECTION_CREATE_PROPERTIES_FORM_NAME));
+        dispatch(change(COLLECTION_CREATE_PROPERTIES_FORM_NAME, PROPERTY_VALUE_FIELD_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
index 9092c7cc..3ab425f1 100644
--- a/src/views-components/collection-properties/update-collection-properties-form.tsx
+++ b/src/views-components/collection-properties/update-collection-properties-form.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reduxForm, reset } from 'redux-form';
+import { reduxForm, change } from 'redux-form';
 import { withStyles } from '@material-ui/core';
 import {
     COLLECTION_UPDATE_FORM_NAME,
@@ -13,6 +13,7 @@ import {
     ResourcePropertiesFormData
 } from 'views-components/resource-properties-form/resource-properties-form';
 import { addPropertyToResourceForm } from 'store/resources/resources-actions';
+import { PROPERTY_VALUE_FIELD_NAME } from 'views-components/resource-properties-form/property-value-field';
 
 const Form = withStyles(
     ({ spacing }) => (
@@ -27,6 +28,6 @@ export const UpdateCollectionPropertiesForm = reduxForm<ResourcePropertiesFormDa
     form: COLLECTION_UPDATE_PROPERTIES_FORM_NAME,
     onSubmit: (data, dispatch) => {
         dispatch(addPropertyToResourceForm(data, COLLECTION_UPDATE_FORM_NAME));
-        dispatch(reset(COLLECTION_UPDATE_PROPERTIES_FORM_NAME));
+        dispatch(change(COLLECTION_UPDATE_PROPERTIES_FORM_NAME, PROPERTY_VALUE_FIELD_NAME, ''));
     }
 })(Form);
\ No newline at end of file
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 8c26523e..5a6d9df6 100644
--- a/src/views-components/project-properties/create-project-properties-form.tsx
+++ b/src/views-components/project-properties/create-project-properties-form.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reduxForm, reset } from 'redux-form';
+import { reduxForm, change } from 'redux-form';
 import { withStyles } from '@material-ui/core';
 import {
     PROJECT_CREATE_PROPERTIES_FORM_NAME,
@@ -13,6 +13,7 @@ import {
     ResourcePropertiesFormData
 } from 'views-components/resource-properties-form/resource-properties-form';
 import { addPropertyToResourceForm } from 'store/resources/resources-actions';
+import { PROPERTY_VALUE_FIELD_NAME } from 'views-components/resource-properties-form/property-value-field';
 
 const Form = withStyles(
     ({ spacing }) => (
@@ -27,6 +28,6 @@ export const CreateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>
     form: PROJECT_CREATE_PROPERTIES_FORM_NAME,
     onSubmit: (data, dispatch) => {
         dispatch(addPropertyToResourceForm(data, PROJECT_CREATE_FORM_NAME));
-        dispatch(reset(PROJECT_CREATE_PROPERTIES_FORM_NAME));
+        dispatch(change(PROJECT_CREATE_PROPERTIES_FORM_NAME, PROPERTY_VALUE_FIELD_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
index 0b5554bc..9bce50ab 100644
--- a/src/views-components/project-properties/update-project-properties-form.tsx
+++ b/src/views-components/project-properties/update-project-properties-form.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reduxForm, reset } from 'redux-form';
+import { reduxForm, change } from 'redux-form';
 import { withStyles } from '@material-ui/core';
 import {
     PROJECT_UPDATE_PROPERTIES_FORM_NAME,
@@ -13,6 +13,7 @@ import {
     ResourcePropertiesFormData
 } from 'views-components/resource-properties-form/resource-properties-form';
 import { addPropertyToResourceForm } from 'store/resources/resources-actions';
+import { PROPERTY_VALUE_FIELD_NAME } from 'views-components/resource-properties-form/property-value-field';
 
 const Form = withStyles(
     ({ spacing }) => (
@@ -27,6 +28,6 @@ export const UpdateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>
     form: PROJECT_UPDATE_PROPERTIES_FORM_NAME,
     onSubmit: (data, dispatch) => {
         dispatch(addPropertyToResourceForm(data, PROJECT_UPDATE_FORM_NAME));
-        dispatch(reset(PROJECT_UPDATE_PROPERTIES_FORM_NAME));
+        dispatch(change(PROJECT_UPDATE_PROPERTIES_FORM_NAME, PROPERTY_VALUE_FIELD_NAME, ''));
     }
 })(Form);
\ No newline at end of file
diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx
index c4dc494b..dad17284 100644
--- a/src/views-components/resource-properties-form/property-field-common.tsx
+++ b/src/views-components/resource-properties-form/property-field-common.tsx
@@ -14,6 +14,7 @@ export interface VocabularyProp {
 
 export interface ValidationProp {
     skipValidation?: boolean;
+    clearPropertyKeyOnSelect?: boolean;
 }
 
 export const mapStateToProps = (state: RootState, ownProps: ValidationProp): VocabularyProp & ValidationProp => ({
diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx
index 0be4527a..0b08ad6d 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -30,9 +30,10 @@ export const PROPERTY_KEY_FIELD_NAME = 'key';
 export const PROPERTY_KEY_FIELD_ID = 'keyID';
 
 export const PropertyKeyField = connectVocabulary(
-    ({ vocabulary, skipValidation }: VocabularyProp & ValidationProp) =>
+    ({ vocabulary, skipValidation, clearPropertyKeyOnSelect }: VocabularyProp & ValidationProp) =>
         <span data-cy='property-field-key'>
         <Field
+            clearPropertyKeyOnSelect
             name={PROPERTY_KEY_FIELD_NAME}
             component={PropertyKeyInput}
             vocabulary={vocabulary}
@@ -40,7 +41,7 @@ export const PropertyKeyField = connectVocabulary(
         </span>
 );
 
-const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
+const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp & { clearPropertyKeyOnSelect?: boolean }) =>
     <FormName children={data => (
         <Autocomplete
             {...buildProps(props)}
@@ -51,6 +52,11 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
                     ? `${s.label} (${s.synonyms.join('; ')})`
                     : s.label
             }
+            onFocus={() => {
+                if (props.clearPropertyKeyOnSelect && props.input.value) {
+                    props.meta.dispatch(reset(props.meta.form));
+                }
+            }}
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={() => {
                 // Case-insensitive search for the key in the vocabulary
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 979d772e..25d0f2bb 100644
--- a/src/views-components/resource-properties-form/resource-properties-form.tsx
+++ b/src/views-components/resource-properties-form/resource-properties-form.tsx
@@ -16,16 +16,17 @@ export interface ResourcePropertiesFormData {
     [PROPERTY_KEY_FIELD_ID]: string;
     [PROPERTY_VALUE_FIELD_NAME]: string;
     [PROPERTY_VALUE_FIELD_ID]: string;
+    clearPropertyKeyOnSelect?: boolean;
 }
 
-export type ResourcePropertiesFormProps = {uuid: string; } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string; }> & WithStyles<GridClassKey>;
+export type ResourcePropertiesFormProps = {uuid: string; clearPropertyKeyOnSelect?: boolean } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string; }> & WithStyles<GridClassKey>;
 
-export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, invalid, classes, uuid }: ResourcePropertiesFormProps ) => {
+export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, invalid, classes, uuid, clearPropertyKeyOnSelect }: ResourcePropertiesFormProps ) => {
     change('uuid', uuid); // Sets the uuid field to the uuid of the resource.
     return <form data-cy='resource-properties-form' onSubmit={handleSubmit}>
         <Grid container spacing={16} classes={classes}>
             <Grid item xs>
-                <PropertyKeyField />
+                <PropertyKeyField clearPropertyKeyOnSelect />
             </Grid>
             <Grid item xs>
                 <PropertyValueField />

commit 5fd300479fd3597b44165517dc61bcc3d9951b14
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri Jun 10 11:58:39 2022 -0300

    Merge branch '19177-sharing-urls-ui-config'. Closes #19177
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/common/config.ts b/src/common/config.ts
index 2518c95e..2954d704 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -50,6 +50,7 @@ export interface ClusterConfigJSON {
         }
     };
     Workbench: {
+        DisableSharingURLsUI: boolean;
         ArvadosDocsite: string;
         FileViewersConfigURL: string;
         WelcomePageHTML: string;
@@ -233,6 +234,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         WebShell: { ExternalURL: "" },
     },
     Workbench: {
+        DisableSharingURLsUI: false,
         ArvadosDocsite: "",
         FileViewersConfigURL: "",
         WelcomePageHTML: "",
diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index 367eea81..cdc6c0c7 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -118,13 +118,14 @@ export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, g
 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
 
     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
+    const sharingURLsDisabled = getState().auth.config.clusterConfig.Workbench.DisableSharingURLsUI;
     if (dialog) {
         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
         try {
             const resourceUuid = dialog.data.resourceUuid;
             await dispatch<any>(initializeManagementForm);
             // For collections, we need to load the public sharing tokens
-            if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
+            if (!sharingURLsDisabled && extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
                 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
                 dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
             }
diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx
new file mode 100644
index 00000000..36447a8d
--- /dev/null
+++ b/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx
@@ -0,0 +1,71 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { mount, configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { Provider } from 'react-redux';
+import { combineReducers, createStore } from 'redux';
+
+import SharingDialogComponent, {
+    SharingDialogComponentProps,
+} from './sharing-dialog-component';
+import {
+    extractUuidObjectType,
+    ResourceObjectType
+} from 'models/resource';
+
+configure({ adapter: new Adapter() });
+
+describe("<SharingDialogComponent />", () => {
+    let props: SharingDialogComponentProps;
+    let store;
+
+    beforeEach(() => {
+        const initialAuthState = {
+            config: {
+                keepWebServiceUrl: 'http://example.com/',
+                keepWebInlineServiceUrl: 'http://*.collections.example.com/',
+            }
+        }
+        store = createStore(combineReducers({
+            auth: (state: any = initialAuthState, action: any) => state,
+        }));
+
+        props = {
+            open: true,
+            loading: false,
+            saveEnabled: false,
+            sharedResourceUuid: 'zzzzz-4zz18-zzzzzzzzzzzzzzz',
+            privateAccess: true,
+            sharingURLsNr: 2,
+            sharingURLsDisabled: false,
+            onClose: jest.fn(),
+            onSave: jest.fn(),
+            onCreateSharingToken: jest.fn(),
+            refreshPermissions: jest.fn(),
+        };
+    });
+
+    it("show sharing urls tab on collections when not disabled", () => {
+        expect(props.sharingURLsDisabled).toBe(false);
+        expect(props.sharingURLsNr).toBe(2);
+        expect(extractUuidObjectType(props.sharedResourceUuid) === ResourceObjectType.COLLECTION).toBe(true);
+        let wrapper = mount(<Provider store={store}><SharingDialogComponent {...props} /></Provider>);
+        expect(wrapper.html()).toContain('Sharing URLs (2)');
+
+        // disable Sharing URLs UI
+        props.sharingURLsDisabled = true;
+        wrapper = mount(<Provider store={store}><SharingDialogComponent {...props} /></Provider>);
+        expect(wrapper.html()).not.toContain('Sharing URLs');
+    });
+
+    it("does not show sharing urls on non-collection resources", () => {
+        props.sharedResourceUuid = 'zzzzz-j7d0g-0123456789abcde';
+        expect(extractUuidObjectType(props.sharedResourceUuid) === ResourceObjectType.COLLECTION).toBe(false);
+        expect(props.sharingURLsDisabled).toBe(false);
+        let wrapper = mount(<Provider store={store}><SharingDialogComponent {...props} /></Provider>);
+        expect(wrapper.html()).not.toContain('Sharing URLs');
+    });
+});
\ No newline at end of file
diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
index 15d7f660..b2f31397 100644
--- a/src/views-components/sharing-dialog/sharing-dialog-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
@@ -47,6 +47,7 @@ export interface SharingDialogDataProps {
     sharedResourceUuid: string;
     sharingURLsNr: number;
     privateAccess: boolean;
+    sharingURLsDisabled: boolean;
 }
 export interface SharingDialogActionProps {
     onClose: () => void;
@@ -58,11 +59,13 @@ enum SharingDialogTab {
     PERMISSIONS = 0,
     URLS = 1,
 }
-export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
+export type SharingDialogComponentProps = SharingDialogDataProps & SharingDialogActionProps;
+
+export default (props: SharingDialogComponentProps) => {
     const { open, loading, saveEnabled, sharedResourceUuid,
-        sharingURLsNr, privateAccess,
+        sharingURLsNr, privateAccess, sharingURLsDisabled,
         onClose, onSave, onCreateSharingToken, refreshPermissions } = props;
-    const showTabs = extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
+    const showTabs = !sharingURLsDisabled && extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
     const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
     const [expDate, setExpDate] = React.useState<Date>();
     const [withExpiration, setWithExpiration] = React.useState<boolean>(false);
@@ -151,7 +154,8 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
                 </Grid>
                 </>
                 }
-                { tabNr === SharingDialogTab.PERMISSIONS && privateAccess && sharingURLsNr > 0 &&
+                { tabNr === SharingDialogTab.PERMISSIONS && !sharingURLsDisabled &&
+                    privateAccess && sharingURLsNr > 0 &&
                 <Grid item md={12}>
                     <Typography variant='caption' align='center' color='error'>
                         Although there aren't specific permissions set, this is publicly accessible via Sharing URL(s).
diff --git a/src/views-components/sharing-dialog/sharing-dialog.tsx b/src/views-components/sharing-dialog/sharing-dialog.tsx
index 6b488e44..01cd390b 100644
--- a/src/views-components/sharing-dialog/sharing-dialog.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog.tsx
@@ -35,18 +35,21 @@ type Props = WithDialogProps<string> & WithProgressStateProps;
 const mapStateToProps = (state: RootState, { working, ...props }: Props): SharingDialogDataProps => {
     const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
     const sharedResourceUuid = dialog?.data.resourceUuid || '';
+    const sharingURLsDisabled = state.auth.config.clusterConfig.Workbench.DisableSharingURLsUI;
     return ({
     ...props,
     saveEnabled: hasChanges(state),
     loading: working,
     sharedResourceUuid,
-    sharingURLsNr: (filterResources(
-        (resource: ApiClientAuthorization) =>
+    sharingURLsDisabled,
+    sharingURLsNr: !sharingURLsDisabled
+        ? (filterResources( (resource: ApiClientAuthorization) =>
             resource.kind === ResourceKind.API_CLIENT_AUTHORIZATION  &&
             resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}`) &&
             resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}/`) &&
             resource.scopes.includes('GET /arvados/v1/keep_services/accessible')
-        )(state.resources) as ApiClientAuthorization[]).length,
+        )(state.resources) as ApiClientAuthorization[]).length
+        : 0,
     privateAccess: getSharingPublicAccessFormData(state)?.visibility === VisibilityLevel.PRIVATE,
     })
 };

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list