[ARVADOS-WORKBENCH2] created: 2.1.0-108-g3b2372cd

Git user git at public.arvados.org
Wed Nov 25 15:19:38 UTC 2020


        at  3b2372cd1ce9528803e32393132b16645aff25f7 (commit)


commit 3b2372cd1ce9528803e32393132b16645aff25f7
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Wed Nov 25 12:19:02 2020 -0300

    17098: Adds integration test checks for the new action.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/cypress/integration/collection-panel.spec.js b/cypress/integration/collection-panel.spec.js
index 2b9957a3..a44edd7e 100644
--- a/cypress/integration/collection-panel.spec.js
+++ b/cypress/integration/collection-panel.spec.js
@@ -352,6 +352,11 @@ describe('Collection panel tests', function() {
             cy.get('[data-cy=collection-files-panel]')
                 .should('contain', 'foo').and('contain', 'bar');
 
+            // Check that old collection action is available on context menu
+            cy.get('[data-cy=collection-panel-options-btn]').click()
+            cy.get('[data-cy=context-menu]').should('contain', 'Recover version')
+            cy.get('body').click(); // Collapse the menu avoiding details panel expansion
+
             // Click on "head version" link, confirm that it's the latest version.
             cy.get('[data-cy=collection-info-panel]').contains('head version').click();
             cy.get('[data-cy=collection-info-panel]')
@@ -362,6 +367,11 @@ describe('Collection panel tests', function() {
             cy.get('[data-cy=collection-files-panel]').
                 should('not.contain', 'foo').and('contain', 'bar');
 
+            // Check that old collection action isn't available on context menu
+            cy.get('[data-cy=collection-panel-options-btn]').click()
+            cy.get('[data-cy=context-menu]').should('not.contain', 'Recover version')
+            cy.get('body').click(); // Collapse the menu avoiding details panel expansion
+
             // Make another change, confirm new version.
             cy.get('[data-cy=collection-panel-options-btn]').click();
             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
diff --git a/src/store/collections/collection-version-actions.ts b/src/store/collections/collection-version-actions.ts
index 68b6a181..007dedcd 100644
--- a/src/store/collections/collection-version-actions.ts
+++ b/src/store/collections/collection-version-actions.ts
@@ -24,7 +24,7 @@ export const recoverVersion = (resourceUuid: string) =>
             dispatch<any>(navigateTo(headVersion.uuid));
         } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: `Couldn't make new version: ${e.errors[0]}`,
+                message: `Couldn't recover version: ${e.errors[0]}`,
                 hideDuration: 2000,
                 kind: SnackbarKind.ERROR
             }));

commit d8e95a7342d85e11eaaf3685c6b1e84ece5f1e92
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Wed Nov 25 12:00:59 2020 -0300

    17098: Fixes unit test for refactored menu kind function.
    
    Added 'redux-mock-store' package that will allow us to unit test dispatched
    actions.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/package.json b/package.json
index 346d4910..c972ff02 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
     "@types/react-router-dom": "4.3.1",
     "@types/react-router-redux": "5.0.16",
     "@types/redux-devtools": "3.0.44",
+    "@types/redux-mock-store": "1.0.2",
     "@types/sinon": "7.5",
     "@types/uuid": "3.4.4",
     "axios-mock-adapter": "1.17.0",
@@ -107,6 +108,7 @@
     "node-sass": "4.9.4",
     "node-sass-chokidar": "1.3.4",
     "redux-devtools": "3.4.1",
+    "redux-mock-store": "1.5.4",
     "typescript": "3.1.1",
     "wait-on": "4.0.2",
     "yamljs": "0.3.0"
diff --git a/src/store/context-menu/context-menu-actions.test.ts b/src/store/context-menu/context-menu-actions.test.ts
index c3e78679..2778568e 100644
--- a/src/store/context-menu/context-menu-actions.test.ts
+++ b/src/store/context-menu/context-menu-actions.test.ts
@@ -2,159 +2,128 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as resource from '~/models/resource';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { resourceKindToContextMenuKind } from './context-menu-actions';
+import { resourceUuidToContextMenuKind } from './context-menu-actions';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
 
 describe('context-menu-actions', () => {
-    describe('resourceKindToContextMenuKind', () => {
-        const uuid = '123';
-
-        describe('ResourceKind.PROJECT', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.PROJECT);
-            });
-
-            it('should return ContextMenuKind.PROJECT_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROJECT_ADMIN);
-            });
-
-            it('should return ContextMenuKind.PROJECT', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROJECT);
-            });
-
-            it('should return ContextMenuKind.READONLY_PROJECT', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.READONLY_PROJECT);
-            });
-        });
-
-        describe('ResourceKind.COLLECTION', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.COLLECTION);
-            });
-
-            it('should return ContextMenuKind.COLLECTION_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.COLLECTION_ADMIN);
-            });
-
-            it('should return ContextMenuKind.COLLECTION', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.COLLECTION);
-            });
-
-            it('should return ContextMenuKind.READONLY_COLLECTION', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.READONLY_COLLECTION);
-            });
-        });
-
-        describe('ResourceKind.PROCESS', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.PROCESS);
-            });
-
-            it('should return ContextMenuKind.PROCESS_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROCESS_ADMIN);
-            });
-
-            it('should return ContextMenuKind.PROCESS_RESOURCE', () => {
-                // given
-                const isAdmin = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROCESS_RESOURCE);
-            });
-        });
-
-        describe('ResourceKind.USER', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.USER);
-            });
-
-            it('should return ContextMenuKind.ROOT_PROJECT', () => {
-                // when
-                const result = resourceKindToContextMenuKind(uuid);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.ROOT_PROJECT);
-            });
-        });
-
-        describe('ResourceKind.LINK', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.LINK);
-            });
-
-            it('should return ContextMenuKind.LINK', () => {
-                // when
-                const result = resourceKindToContextMenuKind(uuid);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.LINK);
+    describe('resourceUuidToContextMenuKind', () => {
+        const middlewares = [thunk];
+        const mockStore = configureStore(middlewares);
+        const userUuid = 'zzzzz-tpzed-bbbbbbbbbbbbbbb';
+        const otherUserUuid = 'zzzzz-tpzed-bbbbbbbbbbbbbbc';
+        const headCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaaa';
+        const oldCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaab';
+        const projectUuid = 'zzzzz-j7d0g-ccccccccccccccc';
+        const linkUuid = 'zzzzz-o0j2j-0123456789abcde';
+        const containerRequestUuid = 'zzzzz-xvhdp-0123456789abcde';
+
+        it('should return the correct menu kind', () => {
+            const cases = [
+                // resourceUuid, isAdminUser, isEditable, isTrashed, expected
+                [headCollectionUuid, false, true, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, false, true, false, ContextMenuKind.COLLECTION],
+                [headCollectionUuid, false, false, true, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, false, false, false, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, true, true, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, true, false, ContextMenuKind.COLLECTION_ADMIN],
+                [headCollectionUuid, true, false, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, false, false, ContextMenuKind.COLLECTION_ADMIN],
+
+                [oldCollectionUuid, false, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+
+                // FIXME: WB2 doesn't currently have context menu for trashed projects
+                // [projectUuid, false, true, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, false, true, false, ContextMenuKind.PROJECT],
+                [projectUuid, false, false, true, ContextMenuKind.READONLY_PROJECT],
+                [projectUuid, false, false, false, ContextMenuKind.READONLY_PROJECT],
+                // [projectUuid, true, true, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, true, false, ContextMenuKind.PROJECT_ADMIN],
+                // [projectUuid, true, false, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, false, false, ContextMenuKind.PROJECT_ADMIN],
+
+                [linkUuid, false, true, true, ContextMenuKind.LINK],
+                [linkUuid, false, true, false, ContextMenuKind.LINK],
+                [linkUuid, false, false, true, ContextMenuKind.LINK],
+                [linkUuid, false, false, false, ContextMenuKind.LINK],
+                [linkUuid, true, true, true, ContextMenuKind.LINK],
+                [linkUuid, true, true, false, ContextMenuKind.LINK],
+                [linkUuid, true, false, true, ContextMenuKind.LINK],
+                [linkUuid, true, false, false, ContextMenuKind.LINK],
+
+                [userUuid, false, true, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, false, ContextMenuKind.ROOT_PROJECT],
+
+                [containerRequestUuid, false, true, true, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, true, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, true, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, true, true, true, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, true, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, true, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, false, ContextMenuKind.PROCESS_ADMIN],
+            ]
+
+            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, expected]) => {
+                const initialState = {
+                    resources: {
+                        [headCollectionUuid]: {
+                            uuid: headCollectionUuid,
+                            ownerUuid: projectUuid,
+                            currentVersionUuid: headCollectionUuid,
+                            isTrashed: isTrashed,
+                        },
+                        [oldCollectionUuid]: {
+                            uuid: oldCollectionUuid,
+                            currentVersionUuid: headCollectionUuid,
+                            isTrashed: isTrashed,
+
+                        },
+                        [projectUuid]: {
+                            uuid: projectUuid,
+                            ownerUuid: isEditable ? userUuid : otherUserUuid,
+                            writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                        },
+                        [linkUuid]: {
+                            uuid: linkUuid,
+                        },
+                        [userUuid]: {
+                            uuid: userUuid,
+                        },
+                        [containerRequestUuid]: {
+                            uuid: containerRequestUuid,
+                            ownerUuid: projectUuid,
+                        },
+                    },
+                    auth: {
+                        user: {
+                            uuid: userUuid,
+                            isAdmin: isAdminUser,
+                        },
+                    },
+                };
+                const store = mockStore(initialState);
+
+                const menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string))
+                try {
+                    expect(menuKind).toBe(expected);
+                } catch (err) {
+                    throw new Error(`menuKind for resource ${JSON.stringify(initialState.resources[resourceUuid as string])} expected to be ${expected} but got ${menuKind}.`);
+                }
             });
         });
     });
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 9a586890..22553885 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -203,13 +203,13 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
 
 export const resourceUuidToContextMenuKind = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const { isAdmin, uuid: userUuid } = getState().auth.user!;
+        const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
         const kind = extractUuidKind(uuid);
         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
-        const isEditable = (resource || {} as EditableResource).isEditable;
+        const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
         switch (kind) {
             case ResourceKind.PROJECT:
-                return !isAdmin
+                return !isAdminUser
                     ? isEditable
                         ? ContextMenuKind.PROJECT
                         : ContextMenuKind.READONLY_PROJECT
@@ -219,17 +219,17 @@ export const resourceUuidToContextMenuKind = (uuid: string) =>
                 if (c === undefined) { return; }
                 const isOldVersion = c.uuid !== c.currentVersionUuid;
                 const isTrashed = c.isTrashed;
-                return (isTrashed && isEditable)
-                    ? ContextMenuKind.TRASHED_COLLECTION
-                    : isOldVersion
-                        ? ContextMenuKind.OLD_VERSION_COLLECTION
-                        : isAdmin
+                return isOldVersion
+                    ? ContextMenuKind.OLD_VERSION_COLLECTION
+                    : (isTrashed && isEditable)
+                        ? ContextMenuKind.TRASHED_COLLECTION
+                        : isAdminUser
                             ? ContextMenuKind.COLLECTION_ADMIN
                             : isEditable
                                 ? ContextMenuKind.COLLECTION
                                 : ContextMenuKind.READONLY_COLLECTION;
             case ResourceKind.PROCESS:
-                return !isAdmin
+                return !isAdminUser
                     ? ContextMenuKind.PROCESS_RESOURCE
                     : ContextMenuKind.PROCESS_ADMIN;
             case ResourceKind.USER:
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
index eb3c5509..696a1362 100644
--- a/src/store/resources/resources.ts
+++ b/src/store/resources/resources.ts
@@ -31,6 +31,8 @@ const getResourceWritableBy = (state: ResourcesState, id: string, userUuid: stri
 
 export const getResourceWithEditableStatus = <T extends EditableResource & GroupResource>(id: string, userUuid?: string) =>
     (state: ResourcesState): T | undefined => {
+        if (state[id] === undefined) { return; }
+
         const resource = JSON.parse(JSON.stringify(state[id] as T));
 
         if (resource) {
diff --git a/yarn.lock b/yarn.lock
index 842d6cf8..d11e22b6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -465,6 +465,13 @@
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
 
+"@types/redux-mock-store at 1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz#c27d5deadfb29d8514bdb0fc2cadae6feea1922d"
+  integrity sha512-6LBtAQBN34i7SI5X+Qs4zpTEZO1tTDZ6sZ9fzFjYwTl3nLQXaBtwYdoV44CzNnyKu438xJ1lSIYyw0YMvunESw==
+  dependencies:
+    redux "^4.0.5"
+
 "@types/shell-quote at 1.6.0":
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.6.0.tgz#537b2949a2ebdcb0d353e448fee45b081021963f"
@@ -6839,6 +6846,11 @@ lodash.isfunction@^3.0.8:
   resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
   integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
 
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
 lodash.isstring@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
@@ -9317,6 +9329,13 @@ redux-form at 7.4.2:
     prop-types "^15.6.1"
     react-lifecycles-compat "^3.0.4"
 
+redux-mock-store at 1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872"
+  integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==
+  dependencies:
+    lodash.isplainobject "^4.0.6"
+
 redux-thunk at 2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
@@ -9348,6 +9367,14 @@ redux@^3.6.0:
     loose-envify "^1.1.0"
     symbol-observable "^1.0.3"
 
+redux@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+  dependencies:
+    loose-envify "^1.4.0"
+    symbol-observable "^1.2.0"
+
 reflect.ownkeys@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"

commit 9ee3502881020e41fa6557ec76a260fce3117cbe
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Nov 23 19:38:16 2020 -0300

    17098: Adds ability to recover an old version collection as the head version.
    
    This includes a proper menu action set for old collections with the new action.
    Also, duplicated code was removed for the collection admin menu action set.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 55c3c5a5..dbb4ccec 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -24,6 +24,7 @@ import DeviceHub from '@material-ui/icons/DeviceHub';
 import Edit from '@material-ui/icons/Edit';
 import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import FlipToFront from '@material-ui/icons/FlipToFront';
 import Folder from '@material-ui/icons/Folder';
 import GetApp from '@material-ui/icons/GetApp';
 import Help from '@material-ui/icons/Help';
@@ -129,6 +130,7 @@ export const RemoveIcon: IconType = (props) => <Delete {...props} />;
 export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
 export const PublicFavoriteIcon: IconType = (props) => <Public {...props} />;
 export const RenameIcon: IconType = (props) => <Edit {...props} />;
+export const RecoverVersionIcon: IconType = (props) => <FlipToFront {...props} />;
 export const RestoreFromTrashIcon: IconType = (props) => <RestoreFromTrash {...props} />;
 export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
 export const SearchIcon: IconType = (props) => <Search {...props} />;
diff --git a/src/index.tsx b/src/index.tsx
index 569656d9..98281b67 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -27,7 +27,7 @@ import { favoriteActionSet } from "~/views-components/context-menu/action-sets/f
 import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set';
 import { collectionFilesItemActionSet, readOnlyCollectionFilesItemActionSet } from '~/views-components/context-menu/action-sets/collection-files-item-action-set';
 import { collectionFilesNotSelectedActionSet } from '~/views-components/context-menu/action-sets/collection-files-not-selected-action-set';
-import { collectionActionSet, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
+import { collectionActionSet, collectionAdminActionSet, oldCollectionVersionActionSet, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
 import { processActionSet } from '~/views-components/context-menu/action-sets/process-action-set';
 import { loadWorkbench } from '~/store/workbench/workbench-actions';
 import { Routes } from '~/routes/routes';
@@ -57,7 +57,6 @@ import { groupActionSet } from '~/views-components/context-menu/action-sets/grou
 import { groupMemberActionSet } from '~/views-components/context-menu/action-sets/group-member-action-set';
 import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set';
 import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions';
-import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set';
 import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
 import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
@@ -78,6 +77,7 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActio
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION_FILES_ITEM, readOnlyCollectionFilesItemActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION, readOnlyCollectionActionSet);
+addMenuActionSet(ContextMenuKind.OLD_VERSION_COLLECTION, oldCollectionVersionActionSet);
 addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
diff --git a/src/store/collections/collection-version-actions.ts b/src/store/collections/collection-version-actions.ts
new file mode 100644
index 00000000..68b6a181
--- /dev/null
+++ b/src/store/collections/collection-version-actions.ts
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "../resources/resources-actions";
+import { navigateTo } from "../navigation/navigation-action";
+
+export const recoverVersion = (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);
+            const { uuid, version, ...rest} = oldVersion;
+            const headVersion = await services.collectionService.update(
+                oldVersion.currentVersionUuid,
+                { ...rest }
+            );
+            dispatch(resourcesActions.SET_RESOURCES([headVersion]));
+            dispatch<any>(navigateTo(headVersion.uuid));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: `Couldn't make new version: ${e.errors[0]}`,
+                hideDuration: 2000,
+                kind: SnackbarKind.ERROR
+            }));
+        }
+    };
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts
index 4b6b9224..7e4c7b98 100644
--- a/src/views-components/context-menu/action-sets/collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-action-set.ts
@@ -5,7 +5,7 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from "~/components/icon/icon";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link, RecoverVersionIcon } from "~/components/icon/icon";
 import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
@@ -16,6 +16,10 @@ import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
+import { recoverVersion } from "~/store/collections/collection-version-actions";
+import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
+import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
+import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
 
 export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     {
@@ -96,3 +100,31 @@ export const collectionActionSet: ContextMenuActionSet = [
         },
     ]
 ];
+
+export const collectionAdminActionSet: ContextMenuActionSet = [
+    [
+        ...collectionActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            component: TogglePublicFavoriteAction,
+            name: 'TogglePublicFavoriteAction',
+            execute: (dispatch, resource) => {
+                dispatch<any>(togglePublicFavorite(resource)).then(() => {
+                    dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+                });
+            }
+        },
+    ]
+];
+
+export const oldCollectionVersionActionSet: ContextMenuActionSet = [
+    [
+        ...readOnlyCollectionActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            icon: RecoverVersionIcon,
+            name: 'Recover version',
+            execute: (dispatch, { uuid }) => {
+                dispatch<any>(recoverVersion(uuid));
+            }
+        },
+    ]
+];
\ No newline at end of file
diff --git a/src/views-components/context-menu/action-sets/collection-admin-action-set.ts b/src/views-components/context-menu/action-sets/collection-admin-action-set.ts
deleted file mode 100644
index 7b39d749..00000000
--- a/src/views-components/context-menu/action-sets/collection-admin-action-set.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from "~/components/icon/icon";
-import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
-import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions";
-import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
-import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
-import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action";
-import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
-import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
-import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
-
-export const collectionAdminActionSet: ContextMenuActionSet = [[
-    {
-        icon: RenameIcon,
-        name: "Edit collection",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionUpdateDialog(resource));
-        }
-    },
-    {
-        icon: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        icon: Link,
-        name: "Copy to clipboard",
-        execute: (dispatch, resource) => {
-            dispatch<any>(copyToClipboardAction(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        component: ToggleFavoriteAction,
-        name: 'ToggleFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        component: TogglePublicFavoriteAction,
-        name: 'TogglePublicFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(togglePublicFavorite(resource)).then(() => {
-                dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
-    },
-    {
-        icon: CopyIcon,
-        name: "Make a copy",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionCopyDialog(resource));
-        }
-
-    },
-    {
-        icon: DetailsIcon,
-        name: "View details",
-        execute: dispatch => {
-            dispatch<any>(toggleDetailsPanel());
-        }
-    },
-    {
-        icon: AdvancedIcon,
-        name: "Advanced",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openAdvancedTabDialog(resource.uuid));
-        }
-    },
-    {
-        component: ToggleTrashAction,
-        name: 'ToggleTrashAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-        }
-    },
-]];
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index b86498a0..219913cd 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -80,6 +80,7 @@ export enum ContextMenuKind {
     COLLECTION = 'Collection',
     COLLECTION_ADMIN = 'CollectionAdmin',
     READONLY_COLLECTION = 'ReadOnlyCollection',
+    OLD_VERSION_COLLECTION = 'OldVersionCollection',
     TRASHED_COLLECTION = 'TrashedCollection',
     PROCESS = "Process",
     PROCESS_ADMIN = 'ProcessAdmin',

commit 4aba4d31b34af60e7cb21b1088723aced0699a69
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Nov 23 19:22:22 2020 -0300

    17098: Refactors context menu's kind decision function.
    
    Over time, the function got several optional arguments that were mostly
    intrinsic of the resource being queried, or the user type. This produced lot
    of duplicated and error prone code on the caller's side.
    Now, all callers only need to provide a uuid, the will get the same result.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 308d5e88..9a586890 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -8,7 +8,6 @@ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { getResource, getResourceWithEditableStatus } from '../resources/resources';
-import { ProjectResource } from '~/models/project';
 import { UserResource } from '~/models/user';
 import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { extractUuidKind, ResourceKind, EditableResource } from '~/models/resource';
@@ -18,6 +17,9 @@ import { SshKeyResource } from '~/models/ssh-key';
 import { VirtualMachinesResource } from '~/models/virtual-machines';
 import { KeepServiceResource } from '~/models/keep-services';
 import { ProcessResource } from '~/models/process';
+import { CollectionResource } from '~/models/collection';
+import { GroupResource } from '~/models/group';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -156,9 +158,8 @@ export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>,
 
 export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const { isAdmin, uuid: userUuid } = getState().auth.user!;
-        const res = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(getState().resources);
-        const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (res || {} as EditableResource).isEditable);
+        const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
+        const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (res && menuKind) {
             dispatch<any>(openContextMenu(event, {
                 name: res.name,
@@ -166,7 +167,7 @@ export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, res
                 kind: res.kind,
                 menuKind,
                 ownerUuid: res.ownerUuid,
-                isTrashed: res.isTrashed
+                isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
             }));
         }
     };
@@ -200,30 +201,42 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
         }
     };
 
-export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean, isEditable?: boolean) => {
-    const kind = extractUuidKind(uuid);
-    switch (kind) {
-        case ResourceKind.PROJECT:
-            return !isAdmin
-                ? isEditable
-                    ? ContextMenuKind.PROJECT
-                    : ContextMenuKind.READONLY_PROJECT
-                : ContextMenuKind.PROJECT_ADMIN;
-        case ResourceKind.COLLECTION:
-            return !isAdmin
-                ? isEditable
-                    ? ContextMenuKind.COLLECTION
-                    : ContextMenuKind.READONLY_COLLECTION
-                : ContextMenuKind.COLLECTION_ADMIN;
-        case ResourceKind.PROCESS:
-            return !isAdmin
-                ? ContextMenuKind.PROCESS_RESOURCE
-                : ContextMenuKind.PROCESS_ADMIN;
-        case ResourceKind.USER:
-            return ContextMenuKind.ROOT_PROJECT;
-        case ResourceKind.LINK:
-            return ContextMenuKind.LINK;
-        default:
-            return;
-    }
-};
+export const resourceUuidToContextMenuKind = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { isAdmin, uuid: userUuid } = getState().auth.user!;
+        const kind = extractUuidKind(uuid);
+        const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
+        const isEditable = (resource || {} as EditableResource).isEditable;
+        switch (kind) {
+            case ResourceKind.PROJECT:
+                return !isAdmin
+                    ? isEditable
+                        ? ContextMenuKind.PROJECT
+                        : ContextMenuKind.READONLY_PROJECT
+                    : ContextMenuKind.PROJECT_ADMIN;
+            case ResourceKind.COLLECTION:
+                const c = getResource<CollectionResource>(uuid)(getState().resources);
+                if (c === undefined) { return; }
+                const isOldVersion = c.uuid !== c.currentVersionUuid;
+                const isTrashed = c.isTrashed;
+                return (isTrashed && isEditable)
+                    ? ContextMenuKind.TRASHED_COLLECTION
+                    : isOldVersion
+                        ? ContextMenuKind.OLD_VERSION_COLLECTION
+                        : isAdmin
+                            ? ContextMenuKind.COLLECTION_ADMIN
+                            : isEditable
+                                ? ContextMenuKind.COLLECTION
+                                : ContextMenuKind.READONLY_COLLECTION;
+            case ResourceKind.PROCESS:
+                return !isAdmin
+                    ? ContextMenuKind.PROCESS_RESOURCE
+                    : ContextMenuKind.PROCESS_ADMIN;
+            case ResourceKind.USER:
+                return ContextMenuKind.ROOT_PROJECT;
+            case ResourceKind.LINK:
+                return ContextMenuKind.LINK;
+            default:
+                return;
+        }
+    };
diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
index 038fea2f..06ea910d 100644
--- a/src/views/collection-content-address-panel/collection-content-address-panel.tsx
+++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
@@ -3,7 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles, Grid, Button } from '@material-ui/core';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Grid,
+    Button
+} from '@material-ui/core';
 import { CollectionIcon } from '~/components/icon/icon';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { BackIcon } from '~/components/icon/icon';
@@ -11,8 +17,10 @@ import { DataTableDefaultView } from '~/components/data-table-default-view/data-
 import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { Dispatch } from 'redux';
-import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
-import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import {
+    resourceUuidToContextMenuKind,
+    openContextMenu
+} from '~/store/context-menu/context-menu-actions';
 import { ResourceKind } from '~/models/resource';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { connect } from 'react-redux';
@@ -20,7 +28,12 @@ import { navigateTo } from '~/store/navigation/navigation-action';
 import { DataColumns } from '~/components/data-table/data-table';
 import { SortDirection } from '~/components/data-table/data-column';
 import { createTree } from '~/models/tree';
-import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate, ResourceStatus } from '~/views-components/data-explorer/renderers';
+import {
+    ResourceName,
+    ResourceOwnerName,
+    ResourceLastModifiedDate,
+    ResourceStatus
+} from '~/views-components/data-explorer/renderers';
 
 type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
 
@@ -105,8 +118,7 @@ export interface CollectionContentAddressPanelActionProps {
 
 const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const isAdmin = dispatch<any>(getIsAdmin());
-        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index b7bd3a62..685bb78b 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -19,8 +19,7 @@ import { CollectionPanelFiles } from '~/views-components/collection-panel-files/
 import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag, navigateToProcess, collectionPanelActions } from '~/store/collection-panel/collection-panel-action';
 import { getResource } from '~/store/resources/resources';
-import { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { openContextMenu, resourceUuidToContextMenuKind } from '~/store/context-menu/context-menu-actions';
 import { formatDate, formatFileSize } from "~/common/formatters";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
@@ -237,19 +236,15 @@ export const CollectionPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {
-                const { uuid, ownerUuid, name, description, kind, isTrashed } = this.props.item;
-                const { isWritable } = this.props;
+                const { uuid, ownerUuid, name, description, kind } = this.props.item;
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(uuid));
                 const resource = {
                     uuid,
                     ownerUuid,
                     name,
                     description,
                     kind,
-                    menuKind: isWritable
-                        ? isTrashed
-                            ? ContextMenuKind.TRASHED_COLLECTION
-                            : ContextMenuKind.COLLECTION
-                        : ContextMenuKind.READONLY_COLLECTION
+                    menuKind,
                 };
                 // Avoid expanding/collapsing the panel
                 event.stopPropagation();
diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx
index cad2f9ba..48a9e330 100644
--- a/src/views/favorite-panel/favorite-panel.tsx
+++ b/src/views/favorite-panel/favorite-panel.tsx
@@ -10,7 +10,7 @@ import { DataColumns } from '~/components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind, EditableResource } from '~/models/resource';
+import { ResourceKind } from '~/models/resource';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
 import {
@@ -22,7 +22,10 @@ import {
     ResourceType
 } from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
@@ -31,8 +34,7 @@ import { RootState } from '~/store/store';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { createTree } from '~/models/tree';
 import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
-import { getResourceWithEditableStatus, ResourcesState } from '~/store/resources/resources';
-import { ProjectResource } from '~/models/project';
+import { ResourcesState } from '~/store/resources/resources';
 
 type CssRules = "toolbar" | "button";
 
@@ -109,7 +111,6 @@ export const favoritePanelColumns: DataColumns<string> = [
 interface FavoritePanelDataProps {
     favorites: FavoritesState;
     resources: ResourcesState;
-    isAdmin: boolean;
     userUuid: string;
 }
 
@@ -121,7 +122,6 @@ interface FavoritePanelActionProps {
 const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({
     favorites: state.favorites,
     resources: state.resources,
-    isAdmin: state.auth.user!.isAdmin,
     userUuid: state.auth.user!.uuid,
 });
 
@@ -133,9 +133,7 @@ export const FavoritePanel = withStyles(styles)(
         class extends React.Component<FavoritePanelProps> {
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: '',
diff --git a/src/views/link-panel/link-panel.tsx b/src/views/link-panel/link-panel.tsx
index 4bff4ee7..f9ec763b 100644
--- a/src/views/link-panel/link-panel.tsx
+++ b/src/views/link-panel/link-panel.tsx
@@ -5,8 +5,15 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { RootState } from '~/store/store';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { LinkPanelRoot, LinkPanelRootActionProps, LinkPanelRootDataProps } from '~/views/link-panel/link-panel-root';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
+import {
+    LinkPanelRoot,
+    LinkPanelRootActionProps,
+    LinkPanelRootDataProps
+} from '~/views/link-panel/link-panel-root';
 import { ResourceKind } from '~/models/resource';
 
 const mapStateToProps = (state: RootState): LinkPanelRootDataProps => {
@@ -17,7 +24,7 @@ const mapStateToProps = (state: RootState): LinkPanelRootDataProps => {
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkPanelRootActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const kind = resourceKindToContextMenuKind(resourceUuid);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 11223f22..47dbd9b0 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -14,7 +14,7 @@ import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind, Resource, EditableResource } from '~/models/resource';
+import { ResourceKind, Resource } from '~/models/resource';
 import {
     ResourceFileSize,
     ResourceLastModifiedDate,
@@ -24,17 +24,26 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
-import { ResourcesState, getResourceWithEditableStatus } from '~/store/resources/resources';
+import {
+    ResourcesState,
+    getResource
+} from '~/store/resources/resources';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ProjectResource } from '~/models/project';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { ArvadosTheme } from "~/common/custom-theme";
 import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from '~/store/resource-type-filters/resource-type-filters';
+import {
+    getInitialResourceTypeFilters,
+    getInitialProcessStatusFilters
+} from '~/store/resource-type-filters/resource-type-filters';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 type CssRules = 'root' | "button";
 
@@ -131,7 +140,6 @@ export const ProjectPanel = withStyles(styles)(
     connect((state: RootState) => ({
         currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
         resources: state.resources,
-        isAdmin: state.auth.user!.isAdmin,
         userUuid: state.auth.user!.uuid,
     }))(
         class extends React.Component<ProjectPanelProps> {
@@ -157,15 +165,15 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const { resources } = this.props;
+                const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: resource.name,
                         uuid: resource.uuid,
                         ownerUuid: resource.ownerUuid,
-                        isTrashed: resource.isTrashed,
+                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false,
                         kind: resource.kind,
                         menuKind
                     }));
diff --git a/src/views/public-favorites-panel/public-favorites-panel.tsx b/src/views/public-favorites-panel/public-favorites-panel.tsx
index 635ac621..800e5e59 100644
--- a/src/views/public-favorites-panel/public-favorites-panel.tsx
+++ b/src/views/public-favorites-panel/public-favorites-panel.tsx
@@ -22,7 +22,10 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { PublicFavoriteIcon } from '~/components/icon/icon';
 import { Dispatch } from 'redux';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
@@ -32,7 +35,6 @@ import { createTree } from '~/models/tree';
 import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
 import { PublicFavoritesState } from '~/store/public-favorites/public-favorites-reducer';
-import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
 
 type CssRules = "toolbar" | "button";
 
@@ -122,8 +124,7 @@ const mapStateToProps = ({ publicFavorites }: RootState): PublicFavoritePanelDat
 
 const mapDispatchToProps = (dispatch: Dispatch): PublicFavoritePanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const isAdmin = dispatch<any>(getIsAdmin());
-        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
diff --git a/src/views/shared-with-me-panel/shared-with-me-panel.tsx b/src/views/shared-with-me-panel/shared-with-me-panel.tsx
index 9b4bcc85..76a314ae 100644
--- a/src/views/shared-with-me-panel/shared-with-me-panel.tsx
+++ b/src/views/shared-with-me-panel/shared-with-me-panel.tsx
@@ -9,14 +9,16 @@ import { connect, DispatchProp } from 'react-redux';
 import { RootState } from '~/store/store';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { ShareMeIcon } from '~/components/icon/icon';
-import { ResourcesState, getResourceWithEditableStatus } from '~/store/resources/resources';
+import { ResourcesState, getResource } from '~/store/resources/resources';
 import { navigateTo } from "~/store/navigation/navigation-action";
 import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { GroupResource } from '~/models/group';
-import { EditableResource } from '~/models/resource';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 type CssRules = "toolbar" | "button";
 
@@ -32,7 +34,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 interface SharedWithMePanelDataProps {
     resources: ResourcesState;
-    isAdmin: boolean;
     userUuid: string;
 }
 
@@ -41,7 +42,6 @@ type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithSt
 export const SharedWithMePanel = withStyles(styles)(
     connect((state: RootState) => ({
         resources: state.resources,
-        isAdmin: state.auth.user!.isAdmin,
         userUuid: state.auth.user!.uuid,
     }))(
         class extends React.Component<SharedWithMePanelProps> {
@@ -56,15 +56,15 @@ export const SharedWithMePanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const { resources } = this.props;
+                const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: '',
                         uuid: resource.uuid,
                         ownerUuid: resource.ownerUuid,
-                        isTrashed: resource.isTrashed,
+                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false,
                         kind: resource.kind,
                         menuKind
                     }));

commit 2e601562bda766fb2ac58e7dbd6a696e6c2371b1
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Nov 23 13:08:18 2020 -0300

    17098: Removes unused code file.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
deleted file mode 100644
index 5bd362f5..00000000
--- a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon } from '~/components/icon/icon';
-import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
-import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
-import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
-import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
-
-export const collectionResourceActionSet: ContextMenuActionSet = [[
-    {
-        icon: RenameIcon,
-        name: "Edit collection",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        component: ToggleFavoriteAction,
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        icon: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveCollectionDialog(resource));
-        }
-    },
-    {
-        icon: CopyIcon,
-        name: "Copy to project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionCopyDialog(resource));
-        }
-    },
-    {
-        icon: DetailsIcon,
-        name: "View details",
-        execute: dispatch => {
-            dispatch<any>(toggleDetailsPanel());
-        }
-    },
-    {
-        icon: AdvancedIcon,
-        name: "Advanced",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openAdvancedTabDialog(resource.uuid));
-        }
-    },
-    {
-        component: ToggleTrashAction,
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-        }
-    },
-    // {
-    //     icon: RemoveIcon,
-    //     name: "Remove",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // }
-]];

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list