[ARVADOS-WORKBENCH2] created: 2.3.2.1

Git user git at public.arvados.org
Fri Jan 7 19:47:48 UTC 2022


        at  6b368635bf1a768e89237c011de76a4230dc9d6d (commit)


commit 6b368635bf1a768e89237c011de76a4230dc9d6d
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Wed Dec 15 19:32:01 2021 -0300

    Merge branch '18584-collection-copy-fix'. Closes #18584.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index eb06a06c..bd211b1a 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -595,6 +595,86 @@ describe('Collection panel tests', function () {
         })
     });
 
+    it('moves a collection to a different project', function () {
+        const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+        const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
+        const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
+
+        cy.createCollection(adminUser.token, {
+            name: collName,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
+        }).as('testCollection');
+        cy.createGroup(adminUser.token, {
+            name: projName,
+            group_class: 'project',
+            owner_uuid: activeUser.user.uuid,
+        }).as('testProject');
+
+        cy.getAll('@testCollection', '@testProject')
+            .then(function ([testCollection, testProject]) {
+                cy.loginAs(activeUser);
+                cy.goToPath(`/collections/${testCollection.uuid}`);
+                cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
+                cy.get('[data-cy=collection-info-panel]')
+                    .should('not.contain', projName)
+                    .and('not.contain', testProject.uuid);
+                cy.get('[data-cy=collection-panel-options-btn]').click();
+                cy.get('[data-cy=context-menu]').contains('Move to').click();
+                cy.get('[data-cy=form-dialog]')
+                    .should('contain', 'Move to')
+                    .within(() => {
+                        cy.get('[data-cy=projects-tree-home-tree-picker]')
+                            .find('i')
+                            .click();
+                        cy.get('[data-cy=projects-tree-home-tree-picker]')
+                            .contains(projName)
+                            .click();
+                    });
+                cy.get('[data-cy=form-submit-btn]').click();
+                cy.get('[data-cy=snackbar]')
+                    .contains('Collection has been moved')
+                cy.get('[data-cy=collection-info-panel]')
+                    .contains(projName).and('contain', testProject.uuid);
+                // Double check that the collection is in the project
+                cy.goToPath(`/projects/${testProject.uuid}`);
+                cy.get('[data-cy=project-panel]').should('contain', collName);
+            });
+    });
+
+    it('makes a copy of an existing collection', function() {
+        const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+        const copyName = `Copy of: ${collName}`;
+
+        cy.createCollection(adminUser.token, {
+            name: collName,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
+        }).as('collection').then(function () {
+            cy.loginAs(activeUser)
+            cy.goToPath(`/collections/${this.collection.uuid}`);
+            cy.get('[data-cy=collection-files-panel]')
+                .should('contain', 'some-file');
+            cy.get('[data-cy=collection-panel-options-btn]').click();
+            cy.get('[data-cy=context-menu]').contains('Make a copy').click();
+            cy.get('[data-cy=form-dialog]')
+                .should('contain', 'Make a copy')
+                .within(() => {
+                    cy.get('[data-cy=projects-tree-home-tree-picker]')
+                        .contains('Projects')
+                        .click();
+                    cy.get('[data-cy=form-submit-btn]').click();
+                });
+            cy.get('[data-cy=snackbar]')
+                .contains('Collection has been copied.')
+            cy.get('[data-cy=snackbar-goto-action]').click();
+            cy.get('[data-cy=project-panel]')
+                .contains(copyName).click();
+            cy.get('[data-cy=collection-files-panel]')
+                .should('contain', 'some-file');
+        });
+    });
+
     it('uses the collection version browser to view a previous version', function () {
         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
 
diff --git a/cypress/integration/favorites.spec.js b/cypress/integration/favorites.spec.js
index 13a2c467..9bc90ebd 100644
--- a/cypress/integration/favorites.spec.js
+++ b/cypress/integration/favorites.spec.js
@@ -44,7 +44,8 @@ describe('Favorites tests', function () {
         });
     });
 
-    it('can copy selected into the collection', () => {
+    // Disabled while addressing #18587
+    it.skip('can copy selected into the collection', () => {
         cy.createCollection(adminUser.token, {
             name: `Test source collection ${Math.floor(Math.random() * 999999)}`,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
diff --git a/src/store/collections/collection-copy-actions.ts b/src/store/collections/collection-copy-actions.ts
index 9d812783..eb9c64fd 100644
--- a/src/store/collections/collection-copy-actions.ts
+++ b/src/store/collections/collection-copy-actions.ts
@@ -12,6 +12,8 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from 'servi
 import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
@@ -27,9 +29,15 @@ export const openCollectionCopyDialog = (resource: { name: string, uuid: string
 export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
+        let collection = getResource<CollectionResource>(resource.uuid)(getState().resources);
         try {
-            const collection = await services.collectionService.get(resource.uuid);
-            const newCollection = await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
+            if (!collection) {
+                collection = await services.collectionService.get(resource.uuid);
+            }
+            const collManifestText = await services.collectionService.get(resource.uuid, undefined, ['manifestText']);
+            collection.manifestText = collManifestText.manifestText;
+            const {href, ...collectionRecord} = collection;
+            const newCollection = await services.collectionService.create({ ...collectionRecord, ownerUuid: resource.ownerUuid, name: resource.name });
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
             return newCollection;
         } catch (e) {
diff --git a/src/store/collections/collection-move-actions.ts b/src/store/collections/collection-move-actions.ts
index d056b6e5..929f1612 100644
--- a/src/store/collections/collection-move-actions.ts
+++ b/src/store/collections/collection-move-actions.ts
@@ -14,6 +14,8 @@ import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
 
 export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
 
@@ -28,13 +30,17 @@ export const openMoveCollectionDialog = (resource: { name: string, uuid: string
 export const moveCollection = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
+        let cachedCollection = getResource<CollectionResource>(resource.uuid)(getState().resources);
         try {
             dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
+            if (!cachedCollection) {
+                cachedCollection = await services.collectionService.get(resource.uuid);
+            }
             const collection = await services.collectionService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
-            return collection;
+            return {...cachedCollection, ...collection};
         } catch (e) {
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index d898c500..9f478d74 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -52,13 +52,13 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
         if (currentCollection) {
             try {
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
-                const collection = await services.collectionService.get(currentCollection.uuid);
+                const collectionManifestText = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
                 const collectionCopy = {
                     name,
                     description,
                     ownerUuid: projectUuid,
                     uuid: undefined,
-                    manifestText: collection.manifestText,
+                    manifestText: collectionManifestText.manifestText,
                 };
                 const newCollection = await services.collectionService.create(collectionCopy);
                 const copiedFiles = await services.collectionService.files(newCollection.uuid);
@@ -67,7 +67,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                     return !paths.find(path => path.indexOf(file.replace(newCollection.uuid, '')) > -1);
                 });
                 await services.collectionService.deleteFiles(
-                    '',
+                    newCollection.uuid,
                     filesToDelete
                 );
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
@@ -135,7 +135,7 @@ export const copyCollectionPartialToSelectedCollection = ({ collectionUuid }: Co
                 });
                 const diffPathToRemove = difference(paths, pathsToRemove);
                 await services.collectionService.deleteFiles(selectedCollection.uuid, pathsToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
-                const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid);
+                const collectionWithDeletedFiles = await services.collectionService.get(collectionUuid, undefined, ['uuid', 'manifestText']);
                 await services.collectionService.update(collectionUuid, { manifestText: `${collectionWithDeletedFiles.manifestText}${(currentCollection.manifestText ? currentCollection.manifestText : currentCollection.unsignedManifestText) || ''}` });
                 await services.collectionService.deleteFiles(collectionWithDeletedFiles.uuid, diffPathToRemove.map(path => path.replace(currentCollection.uuid, collectionUuid)));
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
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 3aeec4c0..59a5f368 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
@@ -26,12 +26,13 @@ export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[
             dispatch<any>(openCollectionPartialCopyDialog());
         }
     },
-    {
-        name: "Copy selected into the collection",
-        execute: dispatch => {
-            dispatch<any>(openCollectionPartialCopyToSelectedCollectionDialog());
-        }
-    }
+    // Disabled while addressing #18587
+    // {
+    //     name: "Copy selected into the collection",
+    //     execute: dispatch => {
+    //         dispatch<any>(openCollectionPartialCopyToSelectedCollectionDialog());
+    //     }
+    // }
 ]];
 
 export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollectionFilesActionSet.concat([[
diff --git a/src/views-components/projects-tree-picker/projects-tree-picker.tsx b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
index 2f3ea611..ee8ce1d5 100644
--- a/src/views-components/projects-tree-picker/projects-tree-picker.tsx
+++ b/src/views-components/projects-tree-picker/projects-tree-picker.tsx
@@ -31,11 +31,17 @@ export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerPro
         disableActivation
     };
     return <div>
-        <HomeTreePicker pickerId={home} {...p} />
-        <SharedTreePicker pickerId={shared} {...p} />
-        <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+        <div data-cy="projects-tree-home-tree-picker">
+            <HomeTreePicker pickerId={home} {...p} />
+        </div>
+        <div data-cy="projects-tree-shared-tree-picker">
+            <SharedTreePicker pickerId={shared} {...p} />
+        </div>
+        <div data-cy="projects-tree-public-favourites-tree-picker">
+            <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
+        </div>
         <div data-cy="projects-tree-favourites-tree-picker">
-            <FavoritesTreePicker pickerId={favorites} {...p} />  
+            <FavoritesTreePicker pickerId={favorites} {...p} />
         </div>
     </div>;
 };
diff --git a/src/views-components/snackbar/snackbar.tsx b/src/views-components/snackbar/snackbar.tsx
index 2a63a31a..a33b6968 100644
--- a/src/views-components/snackbar/snackbar.tsx
+++ b/src/views-components/snackbar/snackbar.tsx
@@ -116,7 +116,7 @@ export const Snackbar = withStyles(styles)(connect(mapStateToProps, mapDispatchT
                 onExited={props.onExited}
                 anchorOrigin={props.anchorOrigin}
                 autoHideDuration={props.autoHideDuration}>
-                <SnackbarContent
+                <div data-cy="snackbar"><SnackbarContent
                     className={classNames(cssClass)}
                     aria-describedby="client-snackbar"
                     message={
@@ -126,7 +126,7 @@ export const Snackbar = withStyles(styles)(connect(mapStateToProps, mapDispatchT
                         </span>
                     }
                     action={actions(props)}
-                />
+                /></div>
             </MaterialSnackbar>
         );
     }
@@ -151,7 +151,7 @@ const actions = (props: SnackbarProps) => {
                 color="inherit"
                 className={classes.linkButton}
                 onClick={() => onClick(link)}>
-                Go To
+                <span data-cy='snackbar-goto-action'>Go To</span>
             </Button>
         );
     }
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 67264511..eaf48d14 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -147,7 +147,7 @@ export const ProjectPanel = withStyles(styles)(
         class extends React.Component<ProjectPanelProps> {
             render() {
                 const { classes } = this.props;
-                return <div className={classes.root}>
+                return <div data-cy='project-panel' className={classes.root}>
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
                         onRowClick={this.handleRowClick}

commit d0df6414bef82439ff46836f059632250d2de3c2
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Wed Dec 1 17:36:55 2021 -0300

    Merge branch '18484-collection-manifest-fix' into main. Closes #18484.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/services/collection-service/collection-service.test.ts b/src/services/collection-service/collection-service.test.ts
index c0aa85f1..b759fd1a 100644
--- a/src/services/collection-service/collection-service.test.ts
+++ b/src/services/collection-service/collection-service.test.ts
@@ -30,6 +30,41 @@ describe('collection-service', () => {
         collectionService.update = jest.fn();
     });
 
+    describe('get', () => {
+        it('should make a list request with uuid filtering', async () => {
+            serverApi.get = jest.fn(() => Promise.resolve(
+                { data: { items: [{}] } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await collectionService.get(uuid);
+            expect(serverApi.get).toHaveBeenCalledWith(
+                '/collections', {
+                    params: {
+                        filters: `[["uuid","=","zzzzz-4zz18-0123456789abcde"]]`,
+                        include_old_versions: true,
+                    },
+                }
+            );
+        });
+
+        it('should be able to request specific fields', async () => {
+            serverApi.get = jest.fn(() => Promise.resolve(
+                { data: { items: [{}] } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await collectionService.get(uuid, undefined, ['manifestText']);
+            expect(serverApi.get).toHaveBeenCalledWith(
+                '/collections', {
+                    params: {
+                        filters: `[["uuid","=","zzzzz-4zz18-0123456789abcde"]]`,
+                        include_old_versions: true,
+                        select: `["manifest_text"]`
+                    },
+                }
+            );
+        });
+    });
+
     describe('update', () => {
         it('should call put selecting updated fields + others', async () => {
             serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 48e797c5..b6272650 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -11,6 +11,8 @@ import { extractFilesData } from "./collection-service-files-response";
 import { TrashableResourceService } from "services/common-service/trashable-resource-service";
 import { ApiActions } from "services/api/api-actions";
 import { customEncodeURI } from "common/url";
+import { FilterBuilder } from "services/api/filter-builder";
+import { ListArguments } from "services/common-service/common-service";
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
@@ -28,6 +30,18 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         ]);
     }
 
+    async get(uuid: string, showErrors?: boolean, select?: string[]) {
+        super.validateUuid(uuid);
+        // We use a filtered list request to avoid getting the manifest text
+        const filters = new FilterBuilder().addEqual('uuid', uuid).getFilters();
+        const listArgs: ListArguments = {filters, includeOldVersions: true};
+        if (select) {
+            listArgs.select = select;
+        }
+        const lst = await super.list(listArgs, showErrors);
+        return lst.items[0];
+    }
+
     create(data?: Partial<CollectionResource>) {
         return super.create({ ...data, preserveVersion: true });
     }
diff --git a/src/services/common-service/common-service.ts b/src/services/common-service/common-service.ts
index 82777342..f66fad74 100644
--- a/src/services/common-service/common-service.ts
+++ b/src/services/common-service/common-service.ts
@@ -68,7 +68,7 @@ export class CommonService<T> {
             }
         }
 
-    private validateUuid(uuid: string) {
+    protected validateUuid(uuid: string) {
         if (uuid === "") {
             throw new Error('UUID cannot be empty string');
         }
@@ -124,18 +124,21 @@ export class CommonService<T> {
         );
     }
 
-    list(args: ListArguments = {}): Promise<ListResults<T>> {
-        const { filters, order, ...other } = args;
+    list(args: ListArguments = {}, showErrors?: boolean): Promise<ListResults<T>> {
+        const { filters, select, ...other } = args;
         const params = {
             ...CommonService.mapKeys(snakeCase)(other),
             filters: filters ? `[${filters}]` : undefined,
-            order: order ? order : undefined
+            select: select
+                ? `[${select.map(snakeCase).map(s => `"${s}"`).join(', ')}]`
+                : undefined
         };
 
         if (QueryString.stringify(params).length <= 1500) {
             return CommonService.defaultResponse(
                 this.serverApi.get(`/${this.resourceType}`, { params }),
-                this.actions
+                this.actions,
+                showErrors
             );
         } else {
             // Using the POST special case to avoid URI length 414 errors.
@@ -152,7 +155,8 @@ export class CommonService<T> {
                         _method: 'GET'
                     }
                 }),
-                this.actions
+                this.actions,
+                showErrors
             );
         }
     }
diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx
index 0f8bf3cb..25d90195 100644
--- a/src/store/advanced-tab/advanced-tab.tsx
+++ b/src/store/advanced-tab/advanced-tab.tsx
@@ -411,7 +411,7 @@ const containerRequestApiResponse = (apiResponse: ContainerRequestResource) => {
 
 const collectionApiResponse = (apiResponse: CollectionResource) => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
-        replicationConfirmedAt, replicationConfirmed, manifestText, deleteAt, trashAt, isTrashed, storageClassesDesired,
+        replicationConfirmedAt, replicationConfirmed, deleteAt, trashAt, isTrashed, storageClassesDesired,
         storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -424,7 +424,6 @@ const collectionApiResponse = (apiResponse: CollectionResource) => {
 "replication_desired": ${stringify(replicationDesired)},
 "replication_confirmed_at": ${stringify(replicationConfirmedAt)},
 "replication_confirmed": ${stringify(replicationConfirmed)},
-"manifest_text": ${stringify(manifestText)},
 "name": ${stringify(name)},
 "description": ${stringify(description)},
 "properties": ${stringifyObject(properties)},
diff --git a/src/store/collections/collection-partial-copy-actions.ts b/src/store/collections/collection-partial-copy-actions.ts
index 49900f2c..d898c500 100644
--- a/src/store/collections/collection-partial-copy-actions.ts
+++ b/src/store/collections/collection-partial-copy-actions.ts
@@ -114,7 +114,7 @@ export const copyCollectionPartialToSelectedCollection = ({ collectionUuid }: Co
         const currentCollection = state.collectionPanel.item;
 
         if (currentCollection && !currentCollection.manifestText) {
-            const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid);
+            const fetchedCurrentCollection = await services.collectionService.get(currentCollection.uuid, undefined, ['manifestText']);
             currentCollection.manifestText = fetchedCurrentCollection.manifestText;
             currentCollection.unsignedManifestText = fetchedCurrentCollection.unsignedManifestText;
         }
diff --git a/src/store/collections/collection-version-actions.ts b/src/store/collections/collection-version-actions.ts
index c0a58432..7d2511ed 100644
--- a/src/store/collections/collection-version-actions.ts
+++ b/src/store/collections/collection-version-actions.ts
@@ -9,6 +9,8 @@ import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { resourcesActions } from "../resources/resources-actions";
 import { navigateTo } from "../navigation/navigation-action";
 import { dialogActions } from "../dialog/dialog-actions";
+import { getResource } from "store/resources/resources";
+import { CollectionResource } from "models/collection";
 
 export const COLLECTION_RESTORE_VERSION_DIALOG = 'collectionRestoreVersionDialog';
 
@@ -28,9 +30,15 @@ export const openRestoreCollectionVersionDialog = (uuid: string) =>
 export const restoreVersion = (resourceUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
-            // Request que entire record because stored old versions usually
-            // don't include the manifest_text field.
-            const oldVersion = await services.collectionService.get(resourceUuid);
+            // Request the manifest text because stored old versions usually
+            // don't include them.
+            let oldVersion = getResource<CollectionResource>(resourceUuid)(getState().resources);
+            if (!oldVersion) {
+                oldVersion = await services.collectionService.get(resourceUuid);
+            }
+            const oldVersionManifest = await services.collectionService.get(resourceUuid, undefined, ['manifestText']);
+            oldVersion.manifestText = oldVersionManifest.manifestText;
+
             const { uuid, version, ...rest} = oldVersion;
             const headVersion = await services.collectionService.update(
                 oldVersion.currentVersionUuid,

commit e9f10ba7ae13cfa4909f0b0dc6ef2911f7d15af3
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Tue Nov 16 14:47:22 2021 -0300

    Merge branch '18215-collection-update-without-manifest' into main.
    Closes #18215
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/favorites.spec.js b/cypress/integration/favorites.spec.js
index 9f4e2b84..13a2c467 100644
--- a/cypress/integration/favorites.spec.js
+++ b/cypress/integration/favorites.spec.js
@@ -150,7 +150,7 @@ describe('Favorites tests', function () {
         cy.getAll('@mySharedWritableProject', '@testTargetCollection')
             .then(function ([mySharedWritableProject, testTargetCollection]) {
                 cy.loginAs(adminUser);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('My Favorites').click();
 
                 const newProjectName = `New project name ${mySharedWritableProject.name}`;
@@ -160,7 +160,7 @@ describe('Favorites tests', function () {
 
                 cy.testEditProjectOrCollection('main', mySharedWritableProject.name, newProjectName, newProjectDescription);
                 cy.testEditProjectOrCollection('main', testTargetCollection.name, newCollectionName, newCollectionDescription, false);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
 
                 cy.get('main').contains(newProjectName).rightclick();
@@ -171,7 +171,7 @@ describe('Favorites tests', function () {
                 cy.get('[data-cy=side-panel-tree]').contains('Public Favorites').click();
 
                 cy.testEditProjectOrCollection('main', newProjectName, mySharedWritableProject.name, 'newProjectDescription');
-                cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false); 
+                cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false);
             });
     });
 
diff --git a/src/services/collection-service/collection-service.test.ts b/src/services/collection-service/collection-service.test.ts
index 061a45ec..c0aa85f1 100644
--- a/src/services/collection-service/collection-service.test.ts
+++ b/src/services/collection-service/collection-service.test.ts
@@ -2,30 +2,53 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { AxiosInstance } from 'axios';
-import { WebDAV } from 'common/webdav';
-import { ApiActions } from '../api/api-actions';
+import axios, { AxiosInstance } from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { CollectionResource } from 'models/collection';
 import { AuthService } from '../auth-service/auth-service';
 import { CollectionService } from './collection-service';
 
 describe('collection-service', () => {
     let collectionService: CollectionService;
-    let serverApi;
+    let serverApi: AxiosInstance;
+    let axiosMock: MockAdapter;
     let webdavClient: any;
     let authService;
     let actions;
 
     beforeEach(() => {
-        serverApi = {} as AxiosInstance;
+        serverApi = axios.create();
+        axiosMock = new MockAdapter(serverApi);
         webdavClient = {
             delete: jest.fn(),
         } as any;
         authService = {} as AuthService;
-        actions = {} as ApiActions;
+        actions = {
+            progressFn: jest.fn(),
+        } as any;
         collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
         collectionService.update = jest.fn();
     });
 
+    describe('update', () => {
+        it('should call put selecting updated fields + others', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const data: Partial<CollectionResource> = {
+                name: 'foo',
+            };
+            const expected = {
+                collection: {
+                    ...data,
+                    preserve_version: true,
+                },
+                select: ['uuid', 'name', 'version', 'modified_at'],
+            }
+            collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
+            await collectionService.update('uuid', data);
+            expect(serverApi.put).toHaveBeenCalledWith('/collections/uuid', expected);
+        });
+    });
+
     describe('deleteFiles', () => {
         it('should remove no files', async () => {
             // given
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 52fbf1a5..48e797c5 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -33,7 +33,8 @@ export class CollectionService extends TrashableResourceService<CollectionResour
     }
 
     update(uuid: string, data: Partial<CollectionResource>) {
-        return super.update(uuid, { ...data, preserveVersion: true });
+        const select = [...Object.keys(data), 'version', 'modifiedAt'];
+        return super.update(uuid, { ...data, preserveVersion: true }, select);
     }
 
     async files(uuid: string) {
diff --git a/src/services/common-service/common-resource-service.ts b/src/services/common-service/common-resource-service.ts
index 66e694a0..c6306779 100644
--- a/src/services/common-service/common-resource-service.ts
+++ b/src/services/common-service/common-resource-service.ts
@@ -37,13 +37,16 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
         return super.create(payload);
     }
 
-    update(uuid: string, data: Partial<T>) {
+    update(uuid: string, data: Partial<T>, select?: string[]) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
             payload = {
                 [this.resourceType.slice(0, -1)]: CommonService.mapKeys(snakeCase)(data),
             };
+            if (select !== undefined && select.length > 0) {
+                payload.select = ['uuid', ...select.map(field => snakeCase(field))];
+            };
         }
         return super.update(uuid, payload);
     }
diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts
index ca9542c5..ee476524 100644
--- a/src/store/collection-panel/collection-panel-action.ts
+++ b/src/store/collection-panel/collection-panel-action.ts
@@ -17,6 +17,7 @@ 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<CollectionResource>(),
@@ -39,7 +40,6 @@ export const loadCollectionPanel = (uuid: string, forceReload = false) =>
         dispatch(resourcesActions.SET_RESOURCES([collection]));
         if (collection.fileCount <= COLLECTION_PANEL_LOAD_FILES_THRESHOLD &&
             !getState().collectionPanel.loadBigCollections) {
-            // dispatch<any>(loadCollectionFiles(collection.uuid));
         }
         return collection;
     };
@@ -52,11 +52,13 @@ export const createCollectionTag = (data: TagProperty) =>
         const properties = Object.assign({}, item.properties);
         const key = data.keyID || data.key;
         const value = data.valueID || data.value;
+        const cachedCollection = getResource<CollectionResource>(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({
@@ -89,11 +91,13 @@ export const deleteCollectionTag = (key: string, value: string) =>
         if (!item) { return; }
 
         const properties = Object.assign({}, item.properties);
+        const cachedCollection = getResource<CollectionResource>(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 }));
diff --git a/src/store/collections/collection-update-actions.ts b/src/store/collections/collection-update-actions.ts
index a9077cfb..04f42b8d 100644
--- a/src/store/collections/collection-update-actions.ts
+++ b/src/store/collections/collection-update-actions.ts
@@ -14,6 +14,7 @@ import { progressIndicatorActions } from "store/progress-indicator/progress-indi
 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";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -36,11 +37,13 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
         dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
 
+        const cachedCollection = getResource<CollectionResource>(collection.uuid)(getState().resources);
         services.collectionService.update(uuid, {
             name: collection.name,
             storageClassesDesired: collection.storageClassesDesired,
             description: collection.description }
         ).then(updatedCollection => {
+            updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 4270cbbd..e78b1f3d 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -120,7 +120,7 @@ export const CollectionPanel = withStyles(styles)(
                 isWritable = true;
             } else {
                 const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
-                if (itemOwner) {
+                if (itemOwner && itemOwner.writableBy) {
                     isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
                 }
             }

commit 176f06e8893b71d4426d8917900a698d8de6a24b
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Wed Dec 1 17:25:35 2021 -0300

    Merge branch '18257-chips-error-fix' into main. Closes #18257.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/components/chips/chips.tsx b/src/components/chips/chips.tsx
index eb68ed7a..c4724d1b 100644
--- a/src/components/chips/chips.tsx
+++ b/src/components/chips/chips.tsx
@@ -38,7 +38,7 @@ export const Chips = withStyles(styles)(
         render() {
             const { values, filler } = this.props;
             return <Grid container spacing={8} className={this.props.classes.root}>
-                {values.map(this.renderChip)}
+                {values && values.map(this.renderChip)}
                 {filler && <Grid item xs>{filler}</Grid>}
             </Grid>;
         }
diff --git a/src/views/run-process-panel/inputs/float-array-input.tsx b/src/views/run-process-panel/inputs/float-array-input.tsx
index 780cbc90..3f0a5334 100644
--- a/src/views/run-process-panel/inputs/float-array-input.tsx
+++ b/src/views/run-process-panel/inputs/float-array-input.tsx
@@ -30,7 +30,7 @@ const validationSelector = createSelector(
 );
 
 const required = (value: string[]) =>
-    value.length > 0
+    value && value.length > 0
         ? undefined
         : ERROR_MESSAGE;
 
diff --git a/src/views/run-process-panel/inputs/int-array-input.tsx b/src/views/run-process-panel/inputs/int-array-input.tsx
index 03cb07ea..8077f28a 100644
--- a/src/views/run-process-panel/inputs/int-array-input.tsx
+++ b/src/views/run-process-panel/inputs/int-array-input.tsx
@@ -30,7 +30,7 @@ const validationSelector = createSelector(
 );
 
 const required = (value: string[]) =>
-    value.length > 0
+    value && value.length > 0
         ? undefined
         : ERROR_MESSAGE;
 
diff --git a/src/views/run-process-panel/inputs/string-array-input.tsx b/src/views/run-process-panel/inputs/string-array-input.tsx
index cabbf749..8955009a 100644
--- a/src/views/run-process-panel/inputs/string-array-input.tsx
+++ b/src/views/run-process-panel/inputs/string-array-input.tsx
@@ -31,7 +31,7 @@ const validationSelector = createSelector(
 );
 
 const required = (value: string[] = []) =>
-    value.length > 0
+    value && value.length > 0
         ? undefined
         : ERROR_MESSAGE;
 

commit 72e38dc2f3ff6f587e23d8512e93a041e7a71ce1
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Tue Nov 30 08:46:51 2021 +0100

    Merge branch '18482-Info-Button-for-projects-is-broken' into main
    closes #18482
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index af2d93e3..1c175952 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -179,4 +179,19 @@ describe('Project tests', function() {
             cy.get('[data-cy=not-found-page]').should('not.exist');
         });
     });
+
+    it('shows details panel when clicking on the info icon', () => {
+        cy.createGroup(activeUser.token, {
+            name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+            group_class: 'project',
+        }).as('testRootProject').then(function(testRootProject) {
+            cy.loginAs(activeUser);
+
+            cy.get('[data-cy=side-panel-tree]').contains(testRootProject.name).click();
+
+            cy.get('[data-cy=additional-info-icon]').click();
+
+            cy.contains(testRootProject.uuid).should('exist');
+        });
+    });
 });
\ No newline at end of file
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
index 10ae1790..480150cb 100644
--- a/src/views-components/main-content-bar/main-content-bar.tsx
+++ b/src/views-components/main-content-bar/main-content-bar.tsx
@@ -60,7 +60,7 @@ export const MainContentBar =
         buttonVisible: isButtonVisible(state),
         projectUuid: state.detailsPanel.resourceUuid,
     }), (dispatch) => ({
-            onDetailsPanelToggle: toggleDetailsPanel,
+            onDetailsPanelToggle: () => dispatch<any>(toggleDetailsPanel()),
             onRefreshButtonClick: (id) => {
                 dispatch<any>(loadSidePanelTreeProjects(id));
                 dispatch<any>(reloadProjectMatchingUuid([id]));
@@ -80,7 +80,7 @@ export const MainContentBar =
                             </Grid>
                             <Grid item>
                                 {props.buttonVisible && <Tooltip title="Additional Info">
-                                    <IconButton color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
+                                    <IconButton data-cy="additional-info-icon" color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
                                         <DetailsIcon />
                                     </IconButton>
                                 </Tooltip>}

commit dffa004174b744e4fb42fb1d63d282f41ecebbc9
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Fri Nov 26 09:05:56 2021 +0100

    Merge branch '18169-cancel-button-not-working' into main
    closes #18169
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/.licenseignore b/.licenseignore
index 853135fc..9b943a1f 100644
--- a/.licenseignore
+++ b/.licenseignore
@@ -14,3 +14,4 @@ public/*
 .npmrc
 src/lib/cwl-svg/*
 tools/arvados_config.yml
+cypress/fixtures/files/5mb.bin
diff --git a/cypress/fixtures/files/5mb.bin b/cypress/fixtures/files/5mb.bin
new file mode 100644
index 00000000..d52f252e
Binary files /dev/null and b/cypress/fixtures/files/5mb.bin differ
diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index 3e06d7e5..eb06a06c 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -793,4 +793,83 @@ describe('Collection panel tests', function () {
                     .contains(adminUser.user.uuid);
             });
     });
+
+    describe('file upload', () => {
+        beforeEach(() => {
+            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('testCollection1');
+        });
+
+        it('allows to cancel running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    cy.get('[data-cy=upload-button]').click();
+
+                    cy.fixture('files/5mb.bin', 'base64').then(content => {
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button').contains('Cancel').click();
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                });
+        });
+
+        it('allows to cancel single file from the running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    cy.get('[data-cy=upload-button]').click();
+
+                    cy.fixture('files/5mb.bin', 'base64').then(content => {
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button[aria-label=Remove]').eq(1).click();
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+
+                        cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
+                    });
+                });
+        });
+
+        it('allows to cancel all files from the running upload', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+                    cy.get('[data-cy=upload-button]').click();
+
+                    cy.fixture('files/5mb.bin', 'base64').then(content => {
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+
+                        cy.get('[data-cy=form-submit-btn]').click();
+
+                        cy.get('button[aria-label=Remove]').click({ multiple: true });
+
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                });
+        });
+    });
 })
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 069ed96d..07290e55 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -280,4 +280,42 @@ Cypress.Commands.add('createProject', ({
             cy.addToFavorites(user.token, user.user.uuid, project.uuid);
         }
     });
-});
\ No newline at end of file
+});
+
+Cypress.Commands.add(
+    'upload',
+    {
+        prevSubject: 'element',
+    },
+    (subject, file, fileName) => {
+        cy.window().then(window => {
+            const blob = b64toBlob(file, '', 512);
+            const testFile = new window.File([blob], fileName);
+
+            cy.wrap(subject).trigger('drop', {
+                dataTransfer: { files: [testFile] },
+            });
+        })
+    }
+)
+
+function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
+    const byteCharacters = atob(b64Data)
+    const byteArrays = []
+
+    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+        const slice = byteCharacters.slice(offset, offset + sliceSize);
+
+        const byteNumbers = new Array(slice.length);
+        for (let i = 0; i < slice.length; i++) {
+            byteNumbers[i] = slice.charCodeAt(i);
+        }
+
+        const byteArray = new Uint8Array(byteNumbers);
+
+        byteArrays.push(byteArray);
+    }
+
+    const blob = new Blob(byteArrays, { type: contentType });
+    return blob
+}
\ No newline at end of file
diff --git a/src/common/webdav.ts b/src/common/webdav.ts
index 758a5e18..93ec21cb 100644
--- a/src/common/webdav.ts
+++ b/src/common/webdav.ts
@@ -84,6 +84,15 @@ export class WebDAV {
                 .keys(headers)
                 .forEach(key => r.setRequestHeader(key, headers[key]));
 
+            if (!(window as any).cancelTokens) {
+                Object.assign(window, { cancelTokens: {} });
+            }
+
+            (window as any).cancelTokens[config.url] = () => { 
+                resolve(r);
+                r.abort();
+            }
+
             if (config.onUploadProgress) {
                 r.upload.addEventListener('progress', config.onUploadProgress);
             }
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index 97cbc8ce..a7001a61 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -517,7 +517,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                         <Button
                             className={classes.uploadButton}
                             data-cy='upload-button'
-                            onClick={onUploadDataClick}
+                            onClick={() => {
+                                if (!collectionAutofetchEnabled) {
+                                    setCollectionAutofetchEnabled(true);
+                                }
+                                onUploadDataClick();
+                            }}
                             variant='contained'
                             color='primary'
                             size='small'>
diff --git a/src/components/file-upload/file-upload.tsx b/src/components/file-upload/file-upload.tsx
index 617529cd..54d5b5db 100644
--- a/src/components/file-upload/file-upload.tsx
+++ b/src/components/file-upload/file-upload.tsx
@@ -123,6 +123,17 @@ export const FileUpload = withStyles(styles)(
             if (!disabled) {
                 onDelete(file);
             }
+
+            let interval = setInterval(() => {
+                const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+
+                if (key) {
+                    clearInterval(interval);
+                    (window as any).cancelTokens[key]();
+                    delete (window as any).cancelTokens[key];
+                }
+            }, 100);
+
         }
         render() {
             const { classes, onDrop, disabled, files } = this.props;
@@ -140,6 +151,7 @@ export const FileUpload = withStyles(styles)(
                             inputs[0].focus();
                         }
                     }}
+                    data-cy="drag-and-drop"
                     disabled={disabled}
                     inputProps={{
                         onFocus: () => {
diff --git a/src/components/form-dialog/form-dialog.tsx b/src/components/form-dialog/form-dialog.tsx
index 19145cea..0fc799de 100644
--- a/src/components/form-dialog/form-dialog.tsx
+++ b/src/components/form-dialog/form-dialog.tsx
@@ -42,7 +42,9 @@ interface DialogProjectDataProps {
     dialogTitle: string;
     formFields: React.ComponentType<InjectedFormProps<any> & WithDialogProps<any>>;
     submitLabel?: string;
+    cancelCallback?: Function;
     enableWhenPristine?: boolean;
+    doNotDisableCancel?: boolean;
 }
 
 type DialogProjectProps = DialogProjectDataProps & WithDialogProps<{}> & InjectedFormProps<any> & WithStyles<CssRules>;
@@ -65,10 +67,18 @@ export const FormDialog = withStyles(styles)((props: DialogProjectProps) =>
             <DialogActions className={props.classes.dialogActions}>
                 <Button
                     data-cy='form-cancel-btn'
-                    onClick={props.closeDialog}
+                    onClick={() => {
+                        props.closeDialog();
+
+                        if (props.cancelCallback) {
+                            props.cancelCallback();
+                            props.reset();
+                            props.initialize({});
+                        }
+                    }}
                     className={props.classes.button}
                     color="primary"
-                    disabled={props.submitting}>
+                    disabled={props.doNotDisableCancel ? false : props.submitting}>
                     {props.cancelLabel || 'Cancel'}
                 </Button>
                 <Button
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 92437806..52fbf1a5 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -107,7 +107,7 @@ export class CollectionService extends TrashableResourceService<CollectionResour
             },
             onUploadProgress: (e: ProgressEvent) => {
                 onProgress(fileId, e.loaded, e.total, Date.now());
-            }
+            },
         };
         return this.webdavClient.upload(fileURL, [file], requestConfig);
     }
diff --git a/src/store/collections/collection-upload-actions.ts b/src/store/collections/collection-upload-actions.ts
index 8f85ea18..0ca681b9 100644
--- a/src/store/collections/collection-upload-actions.ts
+++ b/src/store/collections/collection-upload-actions.ts
@@ -14,12 +14,14 @@ import { progressIndicatorActions } from "store/progress-indicator/progress-indi
 import { collectionPanelFilesAction } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 import { createTree } from 'models/tree';
 import { loadCollectionPanel } from '../collection-panel/collection-panel-action';
+import * as WorkbenchActions from 'store/workbench/workbench-actions';
 
 export const uploadCollectionFiles = (collectionUuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(fileUploaderActions.START_UPLOAD());
         const files = getState().fileUploader.map(file => file.file);
         await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch));
+        dispatch(WorkbenchActions.loadCollection(collectionUuid));
         dispatch(fileUploaderActions.CLEAR_UPLOAD());
     };
 
diff --git a/src/store/file-uploader/file-uploader-actions.ts b/src/store/file-uploader/file-uploader-actions.ts
index 8436c485..a397bbd8 100644
--- a/src/store/file-uploader/file-uploader-actions.ts
+++ b/src/store/file-uploader/file-uploader-actions.ts
@@ -24,6 +24,7 @@ export const fileUploaderActions = unionize({
     SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),
     START_UPLOAD: ofType(),
     DELETE_UPLOAD_FILE: ofType<UploadFile>(),
+    CANCEL_FILES_UPLOAD: ofType(),
 });
 
 export type FileUploaderAction = UnionOf<typeof fileUploaderActions>;
diff --git a/src/store/file-uploader/file-uploader-reducer.ts b/src/store/file-uploader/file-uploader-reducer.ts
index c1f9c681..4218fbee 100644
--- a/src/store/file-uploader/file-uploader-reducer.ts
+++ b/src/store/file-uploader/file-uploader-reducer.ts
@@ -43,6 +43,21 @@ export const fileUploaderReducer = (state: UploaderState = initialState, action:
 
             return updatedState;
         },
+        CANCEL_FILES_UPLOAD: () => {
+            state.forEach((file) => {
+                let interval = setInterval(() => {
+                    const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
+    
+                    if (key) {
+                        clearInterval(interval);
+                        (window as any).cancelTokens[key]();
+                        delete (window as any).cancelTokens[key];
+                    }
+                }, 100);
+            });
+
+            return [];
+        },
         START_UPLOAD: () => {
             const startTime = Date.now();
             return state.map(f => ({ ...f, startTime, prevTime: startTime }));
diff --git a/src/views-components/dialog-upload/dialog-collection-files-upload.tsx b/src/views-components/dialog-upload/dialog-collection-files-upload.tsx
index 2f662bfa..f65bdabf 100644
--- a/src/views-components/dialog-upload/dialog-collection-files-upload.tsx
+++ b/src/views-components/dialog-upload/dialog-collection-files-upload.tsx
@@ -10,16 +10,30 @@ import { FormDialog } from 'components/form-dialog/form-dialog';
 import { require } from 'validators/require';
 import { FileUploaderField } from 'views-components/file-uploader/file-uploader';
 import { WarningCollection } from 'components/warning-collection/warning-collection';
+import { fileUploaderActions } from 'store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 
 type DialogCollectionFilesUploadProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
-export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) =>
-    <FormDialog
+export const DialogCollectionFilesUpload = (props: DialogCollectionFilesUploadProps) => {
+
+    return <FormDialog
         dialogTitle='Upload data'
         formFields={UploadCollectionFilesFields}
         submitLabel='Upload data'
+        doNotDisableCancel
+        cancelCallback={() => {
+            const { submitting, dispatch } = (props as any);
+
+            if (submitting) {
+                dispatch(progressIndicatorActions.STOP_WORKING('uploadCollectionFilesDialog'));
+                dispatch(fileUploaderActions.CANCEL_FILES_UPLOAD());
+                dispatch(fileUploaderActions.CLEAR_UPLOAD());
+            }
+        }}
         {...props}
     />;
+}
 
 const UploadCollectionFilesFields = () => <>
     <Field
diff --git a/src/views-components/file-uploader/file-uploader.tsx b/src/views-components/file-uploader/file-uploader.tsx
index 82e400f7..cde286c4 100644
--- a/src/views-components/file-uploader/file-uploader.tsx
+++ b/src/views-components/file-uploader/file-uploader.tsx
@@ -30,7 +30,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { onDrop }: FileUploaderProps):
             onDrop(files);
         }
     },
-    onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file)),
+    onDelete: file => dispatch(fileUploaderActions.DELETE_UPLOAD_FILE(file))
 });
 
 export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
@@ -38,5 +38,5 @@ export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpl
 export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
     <div>
         <Typography variant='caption'>{props.label}</Typography>
-        <FileUploader disabled={props.meta.submitting} onDrop={props.input.onChange} />
+        <FileUploader disabled={false} onDrop={props.input.onChange} />
     </div>;

commit 9610e9d15c6c00b0684753d1f3ec90d550e700cf
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Sun Nov 14 19:52:15 2021 +0100

    Merge branch '18195-Refresh-Button-does-not-seem-to-refresh-the-sidebar-tree' into main
    closes #18195
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/side-panel.spec.js b/cypress/integration/side-panel.spec.js
index 912e68eb..f9d4dca3 100644
--- a/cypress/integration/side-panel.spec.js
+++ b/cypress/integration/side-panel.spec.js
@@ -114,4 +114,30 @@ describe('Side panel tests', function() {
             });
     });
 
+    it('side panel react to refresh when project data changes', () => {
+        const project = 'writableProject';
+
+        cy.createProject({
+            owningUser: activeUser,
+            targetUser: activeUser,
+            projectName: project,
+            canWrite: true,
+            addToFavorites: false
+        });
+
+        cy.getAll('@writableProject')
+            .then(function ([writableProject]) {
+                cy.loginAs(activeUser);
+                
+                cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+                cy.get('[data-cy=side-panel-tree]').contains(writableProject.name).should('exist');
+
+                cy.trashGroup(activeUser.token, writableProject.uuid);
+
+                cy.contains('Refresh').click();
+
+                cy.contains(writableProject.name).should('not.exist');
+            });
+    });
 })
diff --git a/src/components/refresh-button/refresh-button.tsx b/src/components/refresh-button/refresh-button.tsx
index f2c41d28..9971547b 100644
--- a/src/components/refresh-button/refresh-button.tsx
+++ b/src/components/refresh-button/refresh-button.tsx
@@ -22,13 +22,20 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
 });
 
-export const RefreshButton = ({ history, classes }: RouteComponentProps & WithStyles<CssRules>) =>
+interface RefreshButtonProps {
+    onClick?: () => void;
+}
+
+export const RefreshButton = ({ history, classes, onClick }: RouteComponentProps & WithStyles<CssRules> & RefreshButtonProps) =>
     <Button
         color="primary"
         size="small"
         variant="contained"
         onClick={() => {
             history.replace(window.location.pathname);
+            if (onClick) {
+                onClick();
+            }
         }}
         className={classNames(classes.buttonRight, classes.button)}>
         <ReRunProcessIcon />
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
index 6e1368c0..10ae1790 100644
--- a/src/views-components/main-content-bar/main-content-bar.tsx
+++ b/src/views-components/main-content-bar/main-content-bar.tsx
@@ -12,6 +12,8 @@ import { RootState } from 'store/store';
 import * as Routes from 'routes/routes';
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import RefreshButton from "components/refresh-button/refresh-button";
+import { reloadProjectMatchingUuid } from "store/workbench/workbench-actions";
+import { loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
 
 type CssRules = "infoTooltip";
 
@@ -55,10 +57,15 @@ const isButtonVisible = ({ router }: RootState) => {
 
 export const MainContentBar =
     connect((state: RootState) => ({
-        buttonVisible: isButtonVisible(state)
-    }), {
+        buttonVisible: isButtonVisible(state),
+        projectUuid: state.detailsPanel.resourceUuid,
+    }), (dispatch) => ({
             onDetailsPanelToggle: toggleDetailsPanel,
-        })(
+            onRefreshButtonClick: (id) => {
+                dispatch<any>(loadSidePanelTreeProjects(id));
+                dispatch<any>(reloadProjectMatchingUuid([id]));
+            }
+        }))(
             withStyles(styles)(
                 (props: MainContentBarProps & WithStyles<CssRules> & any) =>
                     <Toolbar>
@@ -67,7 +74,9 @@ export const MainContentBar =
                                 <Breadcrumbs />
                             </Grid>
                             <Grid item>
-                                <RefreshButton />
+                                <RefreshButton onClick={() => {
+                                    props.onRefreshButtonClick(props.projectUuid);
+                                }} />
                             </Grid>
                             <Grid item>
                                 {props.buttonVisible && <Tooltip title="Additional Info">

commit bb1b66c4f7858fb19075d9541ac43ecb7ac955f3
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Thu Nov 11 14:53:00 2021 -0300

    Merge branch '17944-vocabulary-endpoint-retrieval' into main. Closes #17944
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/README.md b/README.md
index 8bb50dbe..4ec4bd1c 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,6 @@ Currently this configuration schema is supported:
 ```
 {
     "API_HOST": "string",
-    "VOCABULARY_URL": "string",
     "FILE_VIEWERS_CONFIG_URL": "string",
 }
 ```
@@ -93,12 +92,6 @@ The Arvados base URL.
 
 The `REACT_APP_ARVADOS_API_HOST` environment variable can be used to set the default URL if the run time configuration is unreachable.
 
-### VOCABULARY_URL
-Local path, or any URL that allows cross-origin requests. See
-[Vocabulary JSON file example](public/vocabulary-example.json).
-
-To use the URL defined in the Arvados cluster configuration, remove the entire `VOCABULARY_URL` entry from the runtime configuration. Found in `/config.json` by default.
-
 ## FILE_VIEWERS_CONFIG_URL
 Local path, or any URL that allows cross-origin requests. See:
 
diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index fb126af6..3e06d7e5 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -97,14 +97,37 @@ describe('Collection panel tests', function () {
                 });
                 // Confirm proper vocabulary labels are displayed on the UI.
                 cy.get('[data-cy=collection-properties-panel]')
-                    .should('contain', 'Color')
-                    .and('contain', 'Magenta');
+                    .should('contain', 'Color: Magenta');
                 // 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');
                     });
+
+                // Case-insensitive on-blur auto-selection test
+                // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
+                cy.get('[data-cy=resource-properties-form]').within(() => {
+                    cy.get('[data-cy=property-field-key]').within(() => {
+                        cy.get('input').type('sIzE');
+                    });
+                    cy.get('[data-cy=property-field-value]').within(() => {
+                        cy.get('input').type('sMaLL');
+                    });
+                    // Cannot "type()" TAB on Cypress so let's click another field
+                    // to trigger the onBlur event.
+                    cy.get('[data-cy=property-field-key]').click();
+                    cy.root().submit();
+                });
+                // Confirm proper vocabulary labels are displayed on the UI.
+                cy.get('[data-cy=collection-properties-panel]')
+                    .should('contain', 'Size: S');
+                // 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.IDTAGSIZES).to.equal('IDVALSIZES2');
+                    });
             });
     });
 
diff --git a/src/common/config.ts b/src/common/config.ts
index 56f7c488..2518c95e 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -51,7 +51,6 @@ export interface ClusterConfigJSON {
     };
     Workbench: {
         ArvadosDocsite: string;
-        VocabularyURL: string;
         FileViewersConfigURL: string;
         WelcomePageHTML: string;
         InactivePageHTML: string;
@@ -204,15 +203,10 @@ remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
                 }
                 config.fileViewersConfigUrl = fileViewerConfigUrl;
 
-                let vocabularyUrl;
                 if (workbenchConfig.VOCABULARY_URL !== undefined) {
-                    warnLocalConfig("VOCABULARY_URL");
-                    vocabularyUrl = workbenchConfig.VOCABULARY_URL;
+                    console.warn(`A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`)
                 }
-                else {
-                    vocabularyUrl = config.clusterConfig.Workbench.VocabularyURL || "/vocabulary-example.json";
-                }
-                config.vocabularyUrl = vocabularyUrl;
+                config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
 
                 return { config, apiHost: workbenchConfig.API_HOST };
             });
@@ -240,7 +234,6 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
     },
     Workbench: {
         ArvadosDocsite: "",
-        VocabularyURL: "",
         FileViewersConfigURL: "",
         WelcomePageHTML: "",
         InactivePageHTML: "",
@@ -315,5 +308,7 @@ const getDefaultConfig = (): WorkbenchConfig => {
 
 export const ARVADOS_API_PATH = "arvados/v1";
 export const CLUSTER_CONFIG_PATH = "arvados/v1/config";
+export const VOCABULARY_PATH = "arvados/v1/vocabulary";
 export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest";
-export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
+export const getClusterConfigURL = (apiHost: string) => `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
+export const getVocabularyURL = (apiHost: string) => `https://${apiHost}/${VOCABULARY_PATH}?nocache=${(new Date()).getTime()}`;
diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts
index 03f28c07..3c542844 100644
--- a/src/models/vocabulary.ts
+++ b/src/models/vocabulary.ts
@@ -47,7 +47,7 @@ export const getTagValueID = (tagKeyID:string, tagValueLabel:string, vocabulary:
     (tagKeyID && vocabulary.tags[tagKeyID] && vocabulary.tags[tagKeyID].values)
     ? Object.keys(vocabulary.tags[tagKeyID].values!).find(
         k => vocabulary.tags[tagKeyID].values![k].labels.find(
-            l => l.label === tagValueLabel) !== undefined) || ''
+            l => l.label.toLowerCase() === tagValueLabel.toLowerCase()) !== undefined) || ''
     : '';
 
 export const getTagValueLabel = (tagKeyID:string, tagValueID:string, vocabulary: Vocabulary) =>
@@ -94,7 +94,7 @@ export const getTags = ({ tags }: Vocabulary) => {
 export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) =>
     Object.keys(vocabulary.tags).find(
         k => vocabulary.tags[k].labels.find(
-            l => l.label === tagKeyLabel) !== undefined
+            l => l.label.toLowerCase() === tagKeyLabel.toLowerCase()) !== undefined
         ) || '';
 
 export const getTagKeyLabel = (tagKeyID:string, vocabulary: Vocabulary) =>
diff --git a/src/services/vocabulary-service/vocabulary-service.ts b/src/services/vocabulary-service/vocabulary-service.ts
index ff2de159..38163f77 100644
--- a/src/services/vocabulary-service/vocabulary-service.ts
+++ b/src/services/vocabulary-service/vocabulary-service.ts
@@ -10,9 +10,9 @@ export class VocabularyService {
         private url: string
     ) { }
 
-    getVocabulary() {
-        return Axios
-            .get<Vocabulary>(this.url)
-            .then(response => response.data);
+    async getVocabulary() {
+        const response = await Axios
+            .get<Vocabulary>(this.url);
+        return response.data;
     }
 }
diff --git a/src/store/vocabulary/vocabulary-actions.ts b/src/store/vocabulary/vocabulary-actions.ts
index 2ca344bb..d73c01fe 100644
--- a/src/store/vocabulary/vocabulary-actions.ts
+++ b/src/store/vocabulary/vocabulary-actions.ts
@@ -10,7 +10,6 @@ import { isVocabulary } from 'models/vocabulary';
 
 export const loadVocabulary = async (dispatch: Dispatch, _: {}, { vocabularyService }: ServiceRepository) => {
     const vocabulary = await vocabularyService.getVocabulary();
-
     dispatch(propertiesActions.SET_PROPERTY({
         key: VOCABULARY_PROPERTY_NAME,
         value: isVocabulary(vocabulary)
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 029d44cc..791949f5 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import { WrappedFieldProps, Field, FormName, reset, change, WrappedFieldInputProps, WrappedFieldMetaProps } from 'redux-form';
 import { memoize } from 'lodash';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, getTags, getTagKeyID } from 'models/vocabulary';
+import { Vocabulary, getTags, getTagKeyID, getTagKeyLabel } from 'models/vocabulary';
 import {
     handleSelect,
     handleBlur,
@@ -39,7 +39,14 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
             label='Key'
             suggestions={getSuggestions(props.input.value, vocabulary)}
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
-            onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
+            onBlur={() => {
+                // Case-insensitive search for the key in the vocabulary
+                const foundKeyID = getTagKeyID(props.input.value, vocabulary);
+                if (foundKeyID !== '') {
+                    props.input.value = getTagKeyLabel(foundKeyID, vocabulary);
+                }
+                handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, foundKeyID)();
+            }}
             onChange={(e: ChangeEvent<HTMLInputElement>) => {
                 const newValue = e.currentTarget.value;
                 handleChange(data.form, props.input, props.meta, newValue);
diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx
index a2b53b3c..b023e412 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import { WrappedFieldProps, Field, formValues, FormName, WrappedFieldInputProps, WrappedFieldMetaProps, change } from 'redux-form';
 import { compose } from 'redux';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, isStrictTag, getTagValues, getTagValueID } from 'models/vocabulary';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel } from 'models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from 'views-components/resource-properties-form/property-key-field';
 import {
     handleSelect,
@@ -60,7 +60,14 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro
             disabled={props.disabled}
             suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
-            onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKeyId, props.input.value, vocabulary))}
+            onBlur={() => {
+                // Case-insensitive search for the value in the vocabulary
+                const foundValueID =  getTagValueID(propertyKeyId, props.input.value, vocabulary);
+                if (foundValueID !== '') {
+                    props.input.value = getTagValueLabel(propertyKeyId, foundValueID, vocabulary);
+                }
+                handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, foundValueID)();
+            }}
             onChange={(e: ChangeEvent<HTMLInputElement>) => {
                 const newValue = e.currentTarget.value;
                 const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary);
diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml
index 369046e6..55dc8a02 100644
--- a/tools/arvados_config.yml
+++ b/tools/arvados_config.yml
@@ -4,6 +4,7 @@ Clusters:
     SystemRootToken: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy
     API:
       RequestTimeout: 30s
+      VocabularyPath: ""
     TLS:
       Insecure: true
     Collections:
diff --git a/public/vocabulary-example.json b/tools/example-vocabulary.json
similarity index 100%
rename from public/vocabulary-example.json
rename to tools/example-vocabulary.json
diff --git a/tools/run-integration-tests.sh b/tools/run-integration-tests.sh
index 159bfc1c..bf4c3ba4 100755
--- a/tools/run-integration-tests.sh
+++ b/tools/run-integration-tests.sh
@@ -70,6 +70,7 @@ echo "ARVADOS_DIR is ${ARVADOS_DIR}"
 
 ARVADOS_LOG=${ARVADOS_DIR}/arvados.log
 ARVADOS_CONF=${WB2_DIR}/tools/arvados_config.yml
+VOCABULARY_CONF=${WB2_DIR}/tools/example-vocabulary.json
 
 if [ ! -f "${WB2_DIR}/src/index.tsx" ]; then
     echo "ERROR: '${WB2_DIR}' isn't workbench2's directory"
@@ -104,6 +105,9 @@ echo "Installing dev dependencies..."
 ~/go/bin/arvados-server install -type test || exit 1
 
 echo "Launching arvados in test mode..."
+VOC_DIR=$(mktemp -d | cut -d \/ -f3) # Removes the /tmp/ part
+cp ${VOCABULARY_CONF} /tmp/${VOC_DIR}/voc.json
+sed -i "s/VocabularyPath: \".*\"/VocabularyPath: \"\/tmp\/${VOC_DIR}\/voc.json\"/" ${ARVADOS_CONF}
 coproc arvboot (~/go/bin/arvados-server boot \
     -type test \
     -config ${ARVADOS_CONF} \

commit 736b6143c9f22e755344ba907aa90669c001386d
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Oct 29 16:46:20 2021 -0400

    Update version of Go used in the docker image.
    
    Add a "make packages-in-docker" target.
    
    no issue #
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/Makefile b/Makefile
index aaf2271c..26361bb1 100644
--- a/Makefile
+++ b/Makefile
@@ -130,5 +130,16 @@ copy: $(DEB_FILE) $(RPM_FILE)
 # use FPM to create DEB and RPM
 packages: copy
 
+packages-in-docker: workbench2-build-image
+	docker run --env ci="true" \
+		--env ARVADOS_DIRECTORY=/tmp/arvados \
+		--env APP_NAME=${APP_NAME} \
+		--env ITERATION=${ITERATION} \
+		--env TARGETS="${TARGETS}" \
+		-w="/tmp/workbench2" \
+		-t -v ${WORKSPACE}:/tmp/workbench2 \
+		-v ${ARVADOS_DIRECTORY}:/tmp/arvados workbench2-build:latest \
+		make packages
+
 workbench2-build-image:
 	(cd docker && docker build -t workbench2-build .)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 729d62c4..3bffcac4 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -11,8 +11,16 @@ RUN apt-get update && \
     libsecret-1-0 libsecret-1-dev rpm ruby ruby-dev rubygems build-essential \
     libpam0g-dev libgbm1 git && \
     apt-get clean
-RUN apt-get -yq --no-install-recommends -t buster-backports install golang-go && \
-    apt-get clean
+
+# Get Go 1.16.9
+RUN cd /usr/src && \
+    wget https://golang.org/dl/go1.16.9.linux-amd64.tar.gz && \
+    tar xzf go1.16.9.linux-amd64.tar.gz && \
+    ln -s /usr/src/go/bin/go /usr/local/bin/go-1.16.9 && \
+    ln -s /usr/src/go/bin/gofmt /usr/local/bin/gofmt-1.16.9 && \
+    ln -s /usr/local/bin/go-1.16.9 /usr/local/bin/go && \
+    ln -s /usr/local/bin/gofmt-1.16.9 /usr/local/bin/gofmt
+
 RUN gem install --no-ri --no-rdoc fpm
 RUN git clone https://git.arvados.org/arvados.git && cd arvados && \
     go mod download && \

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list