[ARVADOS-WORKBENCH2] created: 1.4.1-360-gea0b48ab
Git user
git at public.arvados.org
Wed Jun 10 13:48:24 UTC 2020
at ea0b48ab40307283dc9c7460cc8196040b669491 (commit)
commit ea0b48ab40307283dc9c7460cc8196040b669491
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date: Fri Jun 5 22:35:40 2020 +0200
16437: Removes context items when projects are not editable by user
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
diff --git a/src/index.tsx b/src/index.tsx
index a12dabfa..2cee0540 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -21,7 +21,7 @@ import { CustomTheme } from '~/common/custom-theme';
import { fetchConfig } from '~/common/config';
import { addMenuActionSet, ContextMenuKind } from '~/views-components/context-menu/context-menu';
import { rootProjectActionSet } from "~/views-components/context-menu/action-sets/root-project-action-set";
-import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
+import { projectActionSet, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
import { resourceActionSet } from '~/views-components/context-menu/action-sets/resource-action-set';
import { favoriteActionSet } from "~/views-components/context-menu/action-sets/favorite-action-set";
import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set';
@@ -67,6 +67,7 @@ console.log(`Starting arvados [${getBuildInfo()}]`);
addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
+addMenuActionSet(ContextMenuKind.READONLY_PROJECT, readOnlyProjectActionSet);
addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
diff --git a/src/models/resource.ts b/src/models/resource.ts
index 4708a9da..d8cdd4a0 100644
--- a/src/models/resource.ts
+++ b/src/models/resource.ts
@@ -14,6 +14,10 @@ export interface Resource {
etag: string;
}
+export interface EditableResource extends Resource {
+ isEditable: boolean;
+}
+
export interface TrashableResource extends Resource {
trashAt: string;
deleteAt: string;
diff --git a/src/store/context-menu/context-menu-actions.test.ts b/src/store/context-menu/context-menu-actions.test.ts
new file mode 100644
index 00000000..04657e90
--- /dev/null
+++ b/src/store/context-menu/context-menu-actions.test.ts
@@ -0,0 +1,161 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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';
+
+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_RESOURCE', () => {
+ // given
+ const isAdmin = false;
+ const isEditable = true;
+
+ // when
+ const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
+
+ // then
+ expect(result).toEqual(ContextMenuKind.COLLECTION_RESOURCE);
+ });
+
+ 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);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 2ba6bc2c..1f766bd3 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -7,11 +7,11 @@ import { ContextMenuPosition } from "./context-menu-reducer";
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
-import { getResource } from '../resources/resources';
+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 } from '~/models/resource';
+import { extractUuidKind, ResourceKind, EditableResource } from '~/models/resource';
import { Process } from '~/store/processes/process';
import { RepositoryResource } from '~/models/repositories';
import { SshKeyResource } from '~/models/ssh-key';
@@ -34,6 +34,7 @@ export type ContextMenuResource = {
kind: ResourceKind,
menuKind: ContextMenuKind;
isTrashed?: boolean;
+ isEditable?: boolean;
outputUuid?: string;
workflowUuid?: string;
};
@@ -153,16 +154,17 @@ export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>,
}
};
-export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const res = getResource<ProjectResource>(projectUuid)(getState().resources);
- const isAdmin = getState().auth.user!.isAdmin;
- if (res) {
+ 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);
+ if (res && menuKind) {
dispatch<any>(openContextMenu(event, {
name: res.name,
uuid: res.uuid,
kind: res.kind,
- menuKind: !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN,
+ menuKind,
ownerUuid: res.ownerUuid,
isTrashed: res.isTrashed
}));
@@ -198,13 +200,17 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
}
};
-export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean) => {
+export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean, isEditable?: boolean) => {
const kind = extractUuidKind(uuid);
switch (kind) {
case ResourceKind.PROJECT:
- return !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN;
+ return !isAdmin ?
+ isEditable ? ContextMenuKind.PROJECT : ContextMenuKind.READONLY_PROJECT :
+ ContextMenuKind.PROJECT_ADMIN;
case ResourceKind.COLLECTION:
- return !isAdmin ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.COLLECTION_ADMIN;
+ return !isAdmin ?
+ isEditable ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.READONLY_COLLECTION :
+ ContextMenuKind.COLLECTION_ADMIN;
case ResourceKind.PROCESS:
return !isAdmin ? ContextMenuKind.PROCESS_RESOURCE : ContextMenuKind.PROCESS_ADMIN;
case ResourceKind.USER:
diff --git a/src/store/resources/resources.test.ts b/src/store/resources/resources.test.ts
new file mode 100644
index 00000000..a4c12039
--- /dev/null
+++ b/src/store/resources/resources.test.ts
@@ -0,0 +1,130 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { getResourceWithEditableStatus } from "./resources";
+import { ResourceKind } from "~/models/resource";
+
+describe('resources', () => {
+ describe('getResourceWithEditableStatus', () => {
+ const resourcesState = {
+ 'zzzzz-j7d0g-0123456789ab123': {
+ uuid: 'zzzzz-j7d0g-0123456789ab123',
+ ownerUuid: 'zzzzz-tpzed-0123456789ab789',
+ createdAt: 'string',
+ modifiedByClientUuid: 'string',
+ modifiedByUserUuid: 'string',
+ modifiedAt: 'string',
+ href: 'string',
+ kind: ResourceKind.PROJECT,
+ writableBy: ['zzzzz-tpzed-0123456789ab789'],
+ etag: 'string',
+ },
+ 'zzzzz-4zz18-0123456789ab456': {
+ uuid: 'zzzzz-4zz18-0123456789ab456',
+ ownerUuid: 'zzzzz-j7d0g-0123456789ab123',
+ createdAt: 'string',
+ modifiedByClientUuid: 'string',
+ modifiedByUserUuid: 'string',
+ modifiedAt: 'string',
+ href: 'string',
+ kind: ResourceKind.COLLECTION,
+ etag: 'string',
+ },
+ 'zzzzz-j7d0g-0123456789ab321': {
+ uuid: 'zzzzz-j7d0g-0123456789ab321',
+ ownerUuid: 'zzzzz-tpzed-0123456789ab987',
+ createdAt: 'string',
+ modifiedByClientUuid: 'string',
+ modifiedByUserUuid: 'string',
+ modifiedAt: 'string',
+ href: 'string',
+ kind: ResourceKind.PROJECT,
+ writableBy: ['zzzzz-tpzed-0123456789ab987'],
+ etag: 'string',
+ },
+ 'zzzzz-4zz18-0123456789ab654': {
+ uuid: 'zzzzz-4zz18-0123456789ab654',
+ ownerUuid: 'zzzzz-j7d0g-0123456789ab321',
+ createdAt: 'string',
+ modifiedByClientUuid: 'string',
+ modifiedByUserUuid: 'string',
+ modifiedAt: 'string',
+ href: 'string',
+ kind: ResourceKind.COLLECTION,
+ etag: 'string',
+ },
+ 'zzzzz-tpzed-0123456789abcde': {
+ uuid: 'zzzzz-tpzed-0123456789abcde',
+ ownerUuid: 'zzzzz-tpzed-0123456789abcde',
+ createdAt: 'string',
+ modifiedByClientUuid: 'string',
+ modifiedByUserUuid: 'string',
+ modifiedAt: 'string',
+ href: 'string',
+ kind: ResourceKind.USER,
+ etag: 'string',
+ }
+ };
+
+ it('should return editable user resource (resource UUID is equal to user UUID)', () => {
+ // given
+ const id = 'zzzzz-tpzed-0123456789abcde';
+ const userUuid = 'zzzzz-tpzed-0123456789abcde';
+
+ // when
+ const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+ // then
+ expect(result!.isEditable).toBeTruthy();
+ });
+
+ it('should return editable project resource', () => {
+ // given
+ const id = 'zzzzz-j7d0g-0123456789ab123';
+ const userUuid = 'zzzzz-tpzed-0123456789ab789';
+
+ // when
+ const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+ // then
+ expect(result!.isEditable).toBeTruthy();
+ });
+
+ it('should return editable collection resource', () => {
+ // given
+ const id = 'zzzzz-4zz18-0123456789ab456';
+ const userUuid = 'zzzzz-tpzed-0123456789ab789';
+
+ // when
+ const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+ // then
+ expect(result!.isEditable).toBeTruthy();
+ });
+
+ it('should return not editable project resource', () => {
+ // given
+ const id = 'zzzzz-j7d0g-0123456789ab321';
+ const userUuid = 'zzzzz-tpzed-0123456789ab789';
+
+ // when
+ const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+ // then
+ expect(result!.isEditable).toBeFalsy();
+ });
+
+ it('should return not editable collection resource', () => {
+ // given
+ const id = 'zzzzz-4zz18-0123456789ab654';
+ const userUuid = 'zzzzz-tpzed-0123456789ab789';
+
+ // when
+ const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+ // then
+ expect(result!.isEditable).toBeFalsy();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
index e7153dec..1012209c 100644
--- a/src/store/resources/resources.ts
+++ b/src/store/resources/resources.ts
@@ -2,11 +2,44 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource } from "~/models/resource";
+import { Resource, EditableResource } from "~/models/resource";
import { ResourceKind } from '~/models/resource';
+import { ProjectResource } from "~/models/project";
+import { GroupResource } from "~/models/group";
export type ResourcesState = { [key: string]: Resource };
+const getResourceWritableBy = (state: ResourcesState, id: string, userUuid: string): string[] => {
+ if (!id) {
+ return [];
+ }
+
+ if (id === userUuid) {
+ return [userUuid];
+ }
+
+ const resource = (state[id] as ProjectResource);
+
+ if (!resource) {
+ return [];
+ }
+
+ const { writableBy } = resource;
+
+ return writableBy ? writableBy : getResourceWritableBy(state, resource.ownerUuid, userUuid);
+};
+
+export const getResourceWithEditableStatus = <T extends EditableResource & GroupResource>(id: string, userUuid?: string) =>
+ (state: ResourcesState): T | undefined => {
+ const resource = JSON.parse(JSON.stringify(state[id] as T));
+
+ if (resource) {
+ resource.isEditable = userUuid ? getResourceWritableBy(state, id, userUuid).indexOf(userUuid) > -1 : false;
+ }
+
+ return resource;
+ };
+
export const getResource = <T extends Resource = Resource>(id: string) =>
(state: ResourcesState): T | undefined =>
state[id] as T;
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.test.ts b/src/views-components/context-menu/action-sets/collection-action-set.test.ts
new file mode 100644
index 00000000..9182f3f2
--- /dev/null
+++ b/src/views-components/context-menu/action-sets/collection-action-set.test.ts
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionActionSet, readOnlyCollectionActionSet } from "./collection-action-set";
+
+describe('collection-action-set', () => {
+ const flattCollectionActionSet = collectionActionSet.reduce((prev, next) => prev.concat(next), []);
+ const flattReadOnlyCollectionActionSet = readOnlyCollectionActionSet.reduce((prev, next) => prev.concat(next), []);
+ describe('collectionActionSet', () => {
+ it('should not be empty', () => {
+ // then
+ expect(flattCollectionActionSet.length).toBeGreaterThan(0);
+ });
+
+ it('should contain readOnlyCollectionActionSet items', () => {
+ // then
+ expect(flattCollectionActionSet)
+ .toEqual(expect.arrayContaining(flattReadOnlyCollectionActionSet));
+ })
+ });
+
+ describe('readOnlyCollectionActionSet', () => {
+ it('should not be empty', () => {
+ // then
+ expect(flattReadOnlyCollectionActionSet.length).toBeGreaterThan(0);
+ });
+
+ it('should not contain collectionActionSet items', () => {
+ // then
+ expect(flattReadOnlyCollectionActionSet)
+ .not.toEqual(expect.arrayContaining(flattCollectionActionSet));
+ })
+ });
+});
\ No newline at end of file
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 ea97a9b1..fba2a53a 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
@@ -49,30 +49,33 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
},
]];
-export const collectionActionSet: ContextMenuActionSet = readOnlyCollectionActionSet.concat([[
- {
- icon: RenameIcon,
- name: "Edit collection",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
- },
- {
- component: ToggleTrashAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
- }
- },
-]]);
+export const collectionActionSet: ContextMenuActionSet = [
+ [
+ ...readOnlyCollectionActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ icon: RenameIcon,
+ name: "Edit collection",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openCollectionUpdateDialog(resource));
+ }
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
+ },
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+ }
+ },
+ ]
+];
diff --git a/src/views-components/context-menu/action-sets/project-action-set.test.ts b/src/views-components/context-menu/action-sets/project-action-set.test.ts
new file mode 100644
index 00000000..fd328221
--- /dev/null
+++ b/src/views-components/context-menu/action-sets/project-action-set.test.ts
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { projectActionSet, readOnlyProjectActionSet } from "./project-action-set";
+
+describe('project-action-set', () => {
+ const flattProjectActionSet = projectActionSet.reduce((prev, next) => prev.concat(next), []);
+ const flattReadOnlyProjectActionSet = readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []);
+
+ describe('projectActionSet', () => {
+ it('should not be empty', () => {
+ // then
+ expect(flattProjectActionSet.length).toBeGreaterThan(0);
+ });
+
+ it('should contain readOnlyProjectActionSet items', () => {
+ // then
+ expect(flattProjectActionSet)
+ .toEqual(expect.arrayContaining(flattReadOnlyProjectActionSet));
+ })
+ });
+
+ describe('readOnlyProjectActionSet', () => {
+ it('should not be empty', () => {
+ // then
+ expect(flattReadOnlyProjectActionSet.length).toBeGreaterThan(0);
+ });
+
+ it('should not contain projectActionSet items', () => {
+ // then
+ expect(flattReadOnlyProjectActionSet)
+ .not.toEqual(expect.arrayContaining(flattProjectActionSet));
+ })
+ });
+});
\ No newline at end of file
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index 32616fce..4f92aeb8 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -17,28 +17,7 @@ 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';
-export const projectActionSet: ContextMenuActionSet = [[
- {
- icon: NewProjectIcon,
- name: "New project",
- execute: (dispatch, resource) => {
- dispatch<any>(openProjectCreateDialog(resource.uuid));
- }
- },
- {
- icon: RenameIcon,
- name: "Edit project",
- execute: (dispatch, resource) => {
- dispatch<any>(openProjectUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
+export const readOnlyProjectActionSet: ContextMenuActionSet = [[
{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
@@ -47,20 +26,6 @@ export const projectActionSet: ContextMenuActionSet = [[
});
}
},
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => {
- dispatch<any>(openMoveProjectDialog(resource));
- }
- },
- // {
- // icon: CopyIcon,
- // name: "Copy to project",
- // execute: (dispatch, resource) => {
- // // add code
- // }
- // },
{
icon: DetailsIcon,
name: "View details",
@@ -75,10 +40,44 @@ export const projectActionSet: ContextMenuActionSet = [[
dispatch<any>(openAdvancedTabDialog(resource.uuid));
}
},
- {
- component: ToggleTrashAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
- }
- },
]];
+
+export const projectActionSet: ContextMenuActionSet = [
+ [
+ ...readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openProjectCreateDialog(resource.uuid));
+ }
+ },
+ {
+ icon: RenameIcon,
+ name: "Edit project",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openProjectUpdateDialog(resource));
+ }
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openMoveProjectDialog(resource));
+ }
+ },
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+ }
+ },
+ ]
+];
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index 55b0abd8..db5765ee 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -65,6 +65,7 @@ export enum ContextMenuKind {
API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
ROOT_PROJECT = "RootProject",
PROJECT = "Project",
+ READONLY_PROJECT = 'ReadOnlyProject',
PROJECT_ADMIN = "ProjectAdmin",
RESOURCE = "Resource",
FAVORITE = "Favorite",
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 1e26bc0d..687e17df 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -14,11 +14,11 @@ 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 } from '~/models/resource';
+import { ResourceKind, Resource, EditableResource } from '~/models/resource';
import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
import { ProjectIcon } from '~/components/icon/icon';
import { ResourceName } from '~/views-components/data-explorer/renderers';
-import { ResourcesState, getResource } from '~/store/resources/resources';
+import { ResourcesState, getResourceWithEditableStatus } 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';
@@ -115,6 +115,7 @@ interface ProjectPanelDataProps {
currentItemId: string;
resources: ResourcesState;
isAdmin: boolean;
+ userUuid: string;
}
type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
@@ -124,7 +125,8 @@ export const ProjectPanel = withStyles(styles)(
connect((state: RootState) => ({
currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
resources: state.resources,
- isAdmin: state.auth.user!.isAdmin
+ isAdmin: state.auth.user!.isAdmin,
+ userUuid: state.auth.user!.uuid,
}))(
class extends React.Component<ProjectPanelProps> {
render() {
@@ -149,8 +151,9 @@ export const ProjectPanel = withStyles(styles)(
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
- const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
+ const { isAdmin, userUuid, resources } = this.props;
+ const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
+ const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
if (menuKind && resource) {
this.props.dispatch<any>(openContextMenu(event, {
name: resource.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 7fd00ba1..c9408752 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,13 +9,14 @@ 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, getResource } from '~/store/resources/resources';
+import { ResourcesState, getResourceWithEditableStatus } 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';
type CssRules = "toolbar" | "button";
@@ -32,6 +33,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
interface SharedWithMePanelDataProps {
resources: ResourcesState;
isAdmin: boolean;
+ userUuid: string;
}
type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithStyles<CssRules>;
@@ -39,7 +41,8 @@ type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithSt
export const SharedWithMePanel = withStyles(styles)(
connect((state: RootState) => ({
resources: state.resources,
- isAdmin: state.auth.user!.isAdmin
+ isAdmin: state.auth.user!.isAdmin,
+ userUuid: state.auth.user!.uuid,
}))(
class extends React.Component<SharedWithMePanelProps> {
render() {
@@ -53,8 +56,9 @@ export const SharedWithMePanel = withStyles(styles)(
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
- const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
+ const { isAdmin, userUuid, resources } = this.props;
+ const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(resourceUuid, userUuid)(resources);
+ const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
if (menuKind && resource) {
this.props.dispatch<any>(openContextMenu(event, {
name: '',
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list