From: Lucas Di Pentima Date: Wed, 25 Nov 2020 17:53:29 +0000 (-0300) Subject: 17098: Merge branch 'master' into 17098-old-version-as-head X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/eda7767bc88b7a2a3dc582af47417937ff4de38a?hp=3c2e32130d4229eada68f21d4000da9a0f844ba4 17098: Merge branch 'master' into 17098-old-version-as-head Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress/integration/collection-panel.spec.js b/cypress/integration/collection-panel.spec.js index 377f11d9c9..9c5c656a06 100644 --- a/cypress/integration/collection-panel.spec.js +++ b/cypress/integration/collection-panel.spec.js @@ -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'); }); }); }) diff --git a/package.json b/package.json index 346d4910c1..c972ff0237 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/components/icon/icon.tsx b/src/components/icon/icon.tsx index 55c3c5a50f..dbb4ccec28 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) => ; export const RemoveFavoriteIcon: IconType = (props) => ; export const PublicFavoriteIcon: IconType = (props) => ; export const RenameIcon: IconType = (props) => ; +export const RecoverVersionIcon: IconType = (props) => ; export const RestoreFromTrashIcon: IconType = (props) => ; export const ReRunProcessIcon: IconType = (props) => ; export const SearchIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index 569656d911..98281b67d9 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 0000000000..007dedcdf4 --- /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(navigateTo(headVersion.uuid)); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ + message: `Couldn't recover version: ${e.errors[0]}`, + hideDuration: 2000, + kind: SnackbarKind.ERROR + })); + } + }; diff --git a/src/store/context-menu/context-menu-actions.test.ts b/src/store/context-menu/context-menu-actions.test.ts index c3e7867927..2778568e76 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(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 308d5e8813..225538859a 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, export const openProjectContextMenu = (event: React.MouseEvent, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => { - const { isAdmin, uuid: userUuid } = getState().auth.user!; - const res = getResourceWithEditableStatus(resourceUuid, userUuid)(getState().resources); - const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (res || {} as EditableResource).isEditable); + const res = getResource(resourceUuid)(getState().resources); + const menuKind = dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (res && menuKind) { dispatch(openContextMenu(event, { name: res.name, @@ -166,7 +167,7 @@ export const openProjectContextMenu = (event: React.MouseEvent, 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, 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(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(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; + } + }; diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts index eb3c5509f2..696a136280 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 = (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/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts index 4b6b9224df..a870a813df 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 @@ -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(toggleFavorite(resource)).then(() => { - dispatch(favoritePanelActions.REQUEST_ITEMS()); - }); - } - }, +const toggleFavoriteAction: ContextMenuAction = { + component: ToggleFavoriteAction, + name: 'ToggleFavoriteAction', + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(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(togglePublicFavorite(resource)).then(() => { + dispatch(publicFavoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + ] +]; + +export const oldCollectionVersionActionSet: ContextMenuActionSet = [ + [ + ...commonActionSet.reduce((prev, next) => prev.concat(next), []), + { + icon: RecoverVersionIcon, + name: 'Recover version', + execute: (dispatch, { uuid }) => { + dispatch(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 7b39d74939..0000000000 --- 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(openCollectionUpdateDialog(resource)); - } - }, - { - icon: OpenIcon, - name: "Open in new tab", - execute: (dispatch, resource) => { - dispatch(openInNewTabAction(resource)); - } - }, - { - icon: Link, - name: "Copy to clipboard", - execute: (dispatch, resource) => { - dispatch(copyToClipboardAction(resource)); - } - }, - { - icon: ShareIcon, - name: "Share", - execute: (dispatch, { uuid }) => { - dispatch(openSharingDialog(uuid)); - } - }, - { - component: ToggleFavoriteAction, - name: 'ToggleFavoriteAction', - execute: (dispatch, resource) => { - dispatch(toggleFavorite(resource)).then(() => { - dispatch(favoritePanelActions.REQUEST_ITEMS()); - }); - } - }, - { - component: TogglePublicFavoriteAction, - name: 'TogglePublicFavoriteAction', - execute: (dispatch, resource) => { - dispatch(togglePublicFavorite(resource)).then(() => { - dispatch(publicFavoritePanelActions.REQUEST_ITEMS()); - }); - } - }, - { - icon: MoveToIcon, - name: "Move to", - execute: (dispatch, resource) => dispatch(openMoveCollectionDialog(resource)) - }, - { - icon: CopyIcon, - name: "Make a copy", - execute: (dispatch, resource) => { - dispatch(openCollectionCopyDialog(resource)); - } - - }, - { - icon: DetailsIcon, - name: "View details", - execute: dispatch => { - dispatch(toggleDetailsPanel()); - } - }, - { - icon: AdvancedIcon, - name: "Advanced", - execute: (dispatch, resource) => { - dispatch(openAdvancedTabDialog(resource.uuid)); - } - }, - { - component: ToggleTrashAction, - name: 'ToggleTrashAction', - execute: (dispatch, resource) => { - dispatch(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 index 5bd362f5ff..0000000000 --- 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(openCollectionUpdateDialog(resource)); - } - }, - { - icon: ShareIcon, - name: "Share", - execute: (dispatch, { uuid }) => { - dispatch(openSharingDialog(uuid)); - } - }, - { - component: ToggleFavoriteAction, - execute: (dispatch, resource) => { - dispatch(toggleFavorite(resource)).then(() => { - dispatch(favoritePanelActions.REQUEST_ITEMS()); - }); - } - }, - { - icon: OpenIcon, - name: "Open in new tab", - execute: (dispatch, resource) => { - dispatch(openInNewTabAction(resource)); - } - }, - { - icon: MoveToIcon, - name: "Move to", - execute: (dispatch, resource) => { - dispatch(openMoveCollectionDialog(resource)); - } - }, - { - icon: CopyIcon, - name: "Copy to project", - execute: (dispatch, resource) => { - dispatch(openCollectionCopyDialog(resource)); - } - }, - { - icon: DetailsIcon, - name: "View details", - execute: dispatch => { - dispatch(toggleDetailsPanel()); - } - }, - { - icon: AdvancedIcon, - name: "Advanced", - execute: (dispatch, resource) => { - dispatch(openAdvancedTabDialog(resource.uuid)); - } - }, - { - component: ToggleTrashAction, - execute: (dispatch, resource) => { - dispatch(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!)); - } - }, - // { - // icon: RemoveIcon, - // name: "Remove", - // execute: (dispatch, resource) => { - // // add code - // } - // } -]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index b86498a0e9..219913cdd1 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', 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 038fea2fd4..06ea910d9c 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(getIsAdmin()); - const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin); + const kind = dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (kind) { dispatch(openContextMenu(event, { name: '', diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index b7bd3a62a5..685bb78bda 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) => { - 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(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 cad2f9ba5b..48a9e33029 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 = [ 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 { handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { - const { isAdmin, userUuid, resources } = this.props; - const resource = getResourceWithEditableStatus(resourceUuid, userUuid)(resources); - const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable); + const menuKind = this.props.dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (menuKind) { this.props.dispatch(openContextMenu(event, { name: '', diff --git a/src/views/link-panel/link-panel.tsx b/src/views/link-panel/link-panel.tsx index 4bff4ee7c6..f9ec763bee 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(resourceUuidToContextMenuKind(resourceUuid)); if (kind) { dispatch(openContextMenu(event, { name: '', diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 11223f225e..47dbd9b062 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 { @@ -157,15 +165,15 @@ export const ProjectPanel = withStyles(styles)( } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { - const { isAdmin, userUuid, resources } = this.props; - const resource = getResourceWithEditableStatus(resourceUuid, userUuid)(resources); - const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable); + const { resources } = this.props; + const resource = getResource(resourceUuid)(resources); + const menuKind = this.props.dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (menuKind && resource) { this.props.dispatch(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 635ac6213c..800e5e599f 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(getIsAdmin()); - const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin); + const kind = dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (kind) { dispatch(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 9b4bcc8572..76a314ae30 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 = (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 { @@ -56,15 +56,15 @@ export const SharedWithMePanel = withStyles(styles)( } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { - const { isAdmin, userUuid, resources } = this.props; - const resource = getResourceWithEditableStatus(resourceUuid, userUuid)(resources); - const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable); + const { resources } = this.props; + const resource = getResource(resourceUuid)(resources); + const menuKind = this.props.dispatch(resourceUuidToContextMenuKind(resourceUuid)); if (menuKind && resource) { this.props.dispatch(openContextMenu(event, { name: '', uuid: resource.uuid, ownerUuid: resource.ownerUuid, - isTrashed: resource.isTrashed, + isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false, kind: resource.kind, menuKind })); diff --git a/yarn.lock b/yarn.lock index 842d6cf837..d11e22b659 100644 --- a/yarn.lock +++ b/yarn.lock @@ -465,6 +465,13 @@ "@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"