17098: Merge branch 'master' into 17098-old-version-as-head
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 25 Nov 2020 17:53:29 +0000 (14:53 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 25 Nov 2020 17:53:29 +0000 (14:53 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

20 files changed:
cypress/integration/collection-panel.spec.js
package.json
src/components/icon/icon.tsx
src/index.tsx
src/store/collections/collection-version-actions.ts [new file with mode: 0644]
src/store/context-menu/context-menu-actions.test.ts
src/store/context-menu/context-menu-actions.ts
src/store/resources/resources.ts
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-admin-action-set.ts [deleted file]
src/views-components/context-menu/action-sets/collection-resource-action-set.ts [deleted file]
src/views-components/context-menu/context-menu.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/link-panel/link-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
yarn.lock

index 377f11d9c9882c258fb67e9448e55c70705576c0..9c5c656a0638d4d3ab9f7c75b5bfe5b9a4d3353b 100644 (file)
@@ -352,6 +352,13 @@ describe('Collection panel tests', function() {
             cy.get('[data-cy=collection-files-panel]')
                 .should('contain', 'foo').and('contain', 'bar');
 
+            // Check that only old collection action are available on context menu
+            cy.get('[data-cy=collection-panel-options-btn]').click();
+            cy.get('[data-cy=context-menu]')
+                .should('contain', 'Recover version')
+                .and('not.contain', 'Add to favorites');
+            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 +369,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();
@@ -392,10 +404,23 @@ describe('Collection panel tests', function() {
             // (and now an old version...)
             cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
             cy.get('[data-cy=context-menu]')
-                .should('contain', 'Add to favorites')
+                .should('not.contain', 'Add to favorites')
                 .and('contain', 'Make a copy')
                 .and('not.contain', 'Edit collection');
             cy.get('body').click();
+
+            // Recover first version
+            cy.get('[data-cy=collection-version-browser]').within(() => {
+                cy.get('[data-cy=collection-version-browser-select-1]').click();
+            });
+            cy.get('[data-cy=collection-panel-options-btn]').click()
+            cy.get('[data-cy=context-menu]').contains('Recover version').click();
+            cy.get('[data-cy=collection-info-panel]')
+                .should('not.contain', 'This is an old version');
+            cy.get('[data-cy=collection-version-number]').should('contain', '4');
+            cy.get('[data-cy=collection-info-panel]').should('contain', colName);
+            cy.get('[data-cy=collection-files-panel]')
+                .should('contain', 'foo').and('contain', 'bar');
         });
     });
 })
index 346d4910c19bb51899bf6e71f5626ae5e05aeb5b..c972ff02377218afa2de4afb58cf0a7cafb32676 100644 (file)
@@ -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",
     "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"
index 55c3c5a50f44759f10bbee681299b5ebe92cc32a..dbb4ccec2851b73495b247033e1d0a6dd100dc2c 100644 (file)
@@ -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} />;
index 569656d9117874646b238616330e25d640aa932e..98281b67d9ff5cfb4bca863ea7b5b5f2dd8ce27d 100644 (file)
@@ -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 (file)
index 0000000..007dedc
--- /dev/null
@@ -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 recover version: ${e.errors[0]}`,
+                hideDuration: 2000,
+                kind: SnackbarKind.ERROR
+            }));
+        }
+    };
index c3e78679278f69783ff5c7a055da5def03217729..2778568e7681d1073a7f627e70bf9444b8aaee21 100644 (file)
 //
 // 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}.`);
+                }
             });
         });
     });
index 308d5e88134520296c0088ffef9c662755647a71..225538859a743a690e2da15143fe600d8e786fe6 100644 (file)
@@ -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: isAdminUser, uuid: userUuid } = getState().auth.user!;
+        const kind = extractUuidKind(uuid);
+        const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
+        const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
+        switch (kind) {
+            case ResourceKind.PROJECT:
+                return !isAdminUser
+                    ? 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 isOldVersion
+                    ? ContextMenuKind.OLD_VERSION_COLLECTION
+                    : (isTrashed && isEditable)
+                        ? ContextMenuKind.TRASHED_COLLECTION
+                        : isAdminUser
+                            ? ContextMenuKind.COLLECTION_ADMIN
+                            : isEditable
+                                ? ContextMenuKind.COLLECTION
+                                : ContextMenuKind.READONLY_COLLECTION;
+            case ResourceKind.PROCESS:
+                return !isAdminUser
+                    ? ContextMenuKind.PROCESS_RESOURCE
+                    : ContextMenuKind.PROCESS_ADMIN;
+            case ResourceKind.USER:
+                return ContextMenuKind.ROOT_PROJECT;
+            case ResourceKind.LINK:
+                return ContextMenuKind.LINK;
+            default:
+                return;
+        }
+    };
index eb3c5509f2f1c3f99c2afc028c4ae18e7e308b6c..696a136280c1a72fef39a8a204e5fd9557439508 100644 (file)
@@ -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) {
index 4b6b9224df0f0cb5902ef11301e02739c5371be8..a870a813df80497750ac75b1d17bbf0a04be888a 100644 (file)
@@ -2,10 +2,23 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ContextMenuActionSet } from "../context-menu-action-set";
+import {
+    ContextMenuAction,
+    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,17 +29,22 @@ 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 = [[
-    {
-        component: ToggleFavoriteAction,
-        name: 'ToggleFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
+const toggleFavoriteAction: ContextMenuAction = {
+    component: ToggleFavoriteAction,
+    name: 'ToggleFavoriteAction',
+    execute: (dispatch, resource) => {
+        dispatch<any>(toggleFavorite(resource)).then(() => {
+            dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+        });
+    }
+};
+
+const commonActionSet: ContextMenuActionSet = [[
     {
         icon: OpenIcon,
         name: "Open in new tab",
@@ -65,6 +83,11 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     },
 ]];
 
+export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
+    ...commonActionSet.reduce((prev, next) => prev.concat(next), []),
+    toggleFavoriteAction,
+]];
+
 export const collectionActionSet: ContextMenuActionSet = [
     [
         ...readOnlyCollectionActionSet.reduce((prev, next) => prev.concat(next), []),
@@ -96,3 +119,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 = [
+    [
+        ...commonActionSet.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 (file)
index 7b39d74..0000000
+++ /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/action-sets/collection-resource-action-set.ts b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
deleted file mode 100644 (file)
index 5bd362f..0000000
+++ /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
-    //     }
-    // }
-]];
index b86498a0e9687dfa7fce6e2e2306ee263569ef30..219913cdd13ce4a2549779159a8ab8f62a4be9c7 100644 (file)
@@ -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',
index 038fea2fd44eb4bb3de3d060a3cf163fedd9b6bb..06ea910d9cda3d99d3458257e1ad978dbdd5f4fc 100644 (file)
@@ -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: '',
index b7bd3a62a5cfd54dda1ff6490b84cce37bb517db..685bb78bda561cd90392afc9eecca7078e7b0784 100644 (file)
@@ -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();
index cad2f9ba5b42d3c8f84c5df0db5b6d97694bf7be..48a9e33029d679ec90a4ebe4d35ee966c7cbfad8 100644 (file)
@@ -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: '',
index 4bff4ee7c6be609cba72092cb0a2d43c153e5685..f9ec763bee0804a536ba6804e028817f2e8b82b6 100644 (file)
@@ -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: '',
index 11223f225ec2e6a1bddb08175086e363149dc655..47dbd9b062b665f0c92f08a2d0079bb443833a0f 100644 (file)
@@ -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
                     }));
index 635ac6213c2122c1be60973525ea0e912481c3f3..800e5e599fcedff1ec7578106fc3f74a9c8f688e 100644 (file)
@@ -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: '',
index 9b4bcc8572d890d53d0f62abdfea6f0890e286c5..76a314ae306bd41ff9f56b6118e5c8ba7f45bac8 100644 (file)
@@ -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
                     }));
index 842d6cf837166f6369177b2ee8b64ae9002c0993..d11e22b6599eb2d312cb4e122ef368c7f38a53a4 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
 
+"@types/redux-mock-store@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@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@7.4.2:
     prop-types "^15.6.1"
     react-lifecycles-compat "^3.0.4"
 
+redux-mock-store@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@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"