From 21abfcbaba4e8e735f353a1e3b030dd5dae8465b Mon Sep 17 00:00:00 2001 From: Ward Vandewege Date: Tue, 23 Mar 2021 12:40:56 -0400 Subject: [PATCH] 17119: further changes after review feedback. Arvados-DCO-1.1-Signed-off-by: Ward Vandewege --- src/common/labels.ts | 5 ++- src/index.tsx | 6 ++- .../context-menu/context-menu-actions.test.ts | 15 +++---- .../context-menu/context-menu-actions.ts | 42 +++++------------- .../resource-type-filters.test.ts | 41 ++++++++++++++++- .../resource-type-filters.ts | 44 +++++++++++++++++-- .../side-panel-tree-actions.ts | 2 +- .../action-sets/project-action-set.test.ts | 18 +++++++- .../action-sets/project-action-set.ts | 22 ++++++---- .../action-sets/project-admin-action-set.ts | 15 ++++++- .../context-menu/context-menu.tsx | 2 + .../data-explorer/renderers.tsx | 8 ++-- src/views/project-panel/project-panel.tsx | 10 ++++- 13 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/common/labels.ts b/src/common/labels.ts index c3c4fcd0..cfc2c52c 100644 --- a/src/common/labels.ts +++ b/src/common/labels.ts @@ -4,11 +4,14 @@ import { ResourceKind } from "~/models/resource"; -export const resourceLabel = (type: string) => { +export const resourceLabel = (type: string, subtype = '') => { switch (type) { case ResourceKind.COLLECTION: return "Data collection"; case ResourceKind.PROJECT: + if (subtype === "filter") { + return "Filter group"; + } return "Project"; case ResourceKind.PROCESS: return "Process"; diff --git a/src/index.tsx b/src/index.tsx index 31ae8564..522d8dc1 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, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set"; +import { filterGroupActionSet, 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'; @@ -58,7 +58,7 @@ import { groupMemberActionSet } from '~/views-components/context-menu/action-set import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set'; import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions'; 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 { filterGroupAdminActionSet, projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set'; import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions"; import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action'; import { storeRedirects } from './common/redirect-to'; @@ -68,6 +68,7 @@ console.log(`Starting arvados [${getBuildInfo()}]`); addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet); addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet); addMenuActionSet(ContextMenuKind.READONLY_PROJECT, readOnlyProjectActionSet); +addMenuActionSet(ContextMenuKind.FILTER_GROUP, filterGroupActionSet); addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet); addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet); addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet); @@ -96,6 +97,7 @@ addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet); addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet); addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet); addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet); +addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet); storeRedirects(); diff --git a/src/store/context-menu/context-menu-actions.test.ts b/src/store/context-menu/context-menu-actions.test.ts index 7f6326f7..179e3a3c 100644 --- a/src/store/context-menu/context-menu-actions.test.ts +++ b/src/store/context-menu/context-menu-actions.test.ts @@ -24,7 +24,7 @@ describe('context-menu-actions', () => { it('should return the correct menu kind', () => { const cases = [ - // resourceUuid, isAdminUser, isEditable, isTrashed, inFilterGroup, expected + // resourceUuid, isAdminUser, isEditable, isTrashed, readonly, expected [headCollectionUuid, false, true, true, false, ContextMenuKind.TRASHED_COLLECTION], [headCollectionUuid, false, true, false, false, ContextMenuKind.COLLECTION], [headCollectionUuid, false, true, false, true, ContextMenuKind.READONLY_COLLECTION], @@ -87,10 +87,10 @@ describe('context-menu-actions', () => { [containerRequestUuid, true, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE], ] - cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, inFilterGroup, expected]) => { + cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, readonly, expected]) => { const initialState = { properties: { - [PROJECT_PANEL_CURRENT_UUID]: inFilterGroup ? filterGroupUuid : projectUuid, + [PROJECT_PANEL_CURRENT_UUID]: projectUuid, }, resources: { [headCollectionUuid]: { @@ -133,20 +133,15 @@ describe('context-menu-actions', () => { isAdmin: isAdminUser, }, }, - router: { - location: { - pathname: inFilterGroup ? "/projects/" + filterGroupUuid : "", - }, - }, }; const store = mockStore(initialState); let menuKind: any; try { - menuKind = store.dispatch(resourceUuidToContextMenuKind(resourceUuid as string)) + menuKind = store.dispatch(resourceUuidToContextMenuKind(resourceUuid as string, readonly as boolean)) 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}.`); + throw new Error(`menuKind for resource ${JSON.stringify(initialState.resources[resourceUuid as string])} readonly: ${readonly} 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 f8049a5c..83335f83 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -20,9 +20,6 @@ import { ProcessResource } from '~/models/process'; import { CollectionResource } from '~/models/collection'; import { GroupClass, GroupResource } from '~/models/group'; import { GroupContentsResource } from '~/services/groups-service/groups-service'; -import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action'; -import { matchProjectRoute } from '~/routes/routes'; -import { RouterState } from "react-router-redux"; export const contextMenuActions = unionize({ OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(), @@ -204,38 +201,23 @@ export const openProcessContextMenu = (event: React.MouseEvent, pro } }; -export const isProjectRoute = (router: RouterState) => { - const pathname = router.location ? router.location.pathname : ''; - const matchProject = matchProjectRoute(pathname); - return Boolean(matchProject); -}; - -export const resourceUuidToContextMenuKind = (uuid: string) => +export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) => (dispatch: Dispatch, getState: () => RootState) => { const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!; const kind = extractUuidKind(uuid); const resource = getResourceWithEditableStatus(uuid, userUuid)(getState().resources); - // When viewing the contents of a filter group, all contents should be treated as read only. - let inFilterGroup = false; - const { router } = getState(); - if (isProjectRoute(router)) { - const projectUuid = getProjectPanelCurrentUuid(getState()); - if (projectUuid !== undefined) { - const project = getResource(projectUuid)(getState().resources); - if (project && project.groupClass === GroupClass.FILTER) { - inFilterGroup = true; - } - } - } - - const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !inFilterGroup; + const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly; switch (kind) { case ResourceKind.PROJECT: - return (isAdminUser && !inFilterGroup) - ? ContextMenuKind.PROJECT_ADMIN + return (isAdminUser && !readonly) + ? (resource && resource.groupClass !== GroupClass.FILTER) + ? ContextMenuKind.PROJECT_ADMIN + : ContextMenuKind.FILTER_GROUP_ADMIN : isEditable - ? ContextMenuKind.PROJECT + ? (resource && resource.groupClass !== GroupClass.FILTER) + ? ContextMenuKind.PROJECT + : ContextMenuKind.FILTER_GROUP : ContextMenuKind.READONLY_PROJECT; case ResourceKind.COLLECTION: const c = getResource(uuid)(getState().resources); @@ -246,15 +228,15 @@ export const resourceUuidToContextMenuKind = (uuid: string) => ? ContextMenuKind.OLD_VERSION_COLLECTION : (isTrashed && isEditable) ? ContextMenuKind.TRASHED_COLLECTION - : (isAdminUser && !inFilterGroup) + : (isAdminUser && !readonly) ? ContextMenuKind.COLLECTION_ADMIN : isEditable ? ContextMenuKind.COLLECTION : ContextMenuKind.READONLY_COLLECTION; case ResourceKind.PROCESS: - return (isAdminUser && !inFilterGroup) + return (isAdminUser && !readonly) ? ContextMenuKind.PROCESS_ADMIN - : inFilterGroup + : readonly ? ContextMenuKind.READONLY_PROCESS_RESOURCE : ContextMenuKind.PROCESS_RESOURCE; case ResourceKind.USER: diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts index 2f4d3cad..95d0349f 100644 --- a/src/store/resource-type-filters/resource-type-filters.test.ts +++ b/src/store/resource-type-filters/resource-type-filters.test.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter } from './resource-type-filters'; +import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter } from './resource-type-filters'; import { ResourceKind } from '~/models/resource'; import { deselectNode } from '~/models/tree'; import { pipe } from 'lodash/fp'; @@ -73,4 +73,43 @@ describe("serializeResourceTypeFilters", () => { expect(serializedFilters) .toEqual(`["uuid","is_a",["${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","!=",null]`); }); + + it("should serialize all project types", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(ObjectTypeFilter.COLLECTION), + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]]`); + }); + + it("should serialize filter groups", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(GroupTypeFilter.PROJECT) + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(ObjectTypeFilter.COLLECTION), + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]],["groups.group_class","=","filter"]`); + }); + + it("should serialize projects (normal)", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(GroupTypeFilter.FILTER_GROUP) + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(ObjectTypeFilter.COLLECTION), + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]],["groups.group_class","=","project"]`); + }); + }); diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts index ef1198bc..26db4e9e 100644 --- a/src/store/resource-type-filters/resource-type-filters.ts +++ b/src/store/resource-type-filters/resource-type-filters.ts @@ -25,7 +25,12 @@ export enum ProcessStatusFilter { export enum ObjectTypeFilter { PROJECT = 'Project', PROCESS = 'Process', - COLLECTION = 'Data Collection', + COLLECTION = 'Data collection', +} + +export enum GroupTypeFilter { + PROJECT = 'Project (normal)', + FILTER_GROUP = 'Filter group', } export enum CollectionTypeFilter { @@ -62,7 +67,11 @@ export const getSimpleObjectTypeFilters = pipe( // causing compile issues. export const getInitialResourceTypeFilters = pipe( (): DataTableFilters => createTree(), - initFilter(ObjectTypeFilter.PROJECT), + pipe( + initFilter(ObjectTypeFilter.PROJECT), + initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT), + initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT), + ), pipe( initFilter(ObjectTypeFilter.PROCESS), initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.PROCESS), @@ -124,10 +133,14 @@ const objectTypeToResourceKind = (type: ObjectTypeFilter) => { }; const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType) => { + const groupFilters = getMatchingFilters(values(GroupTypeFilter), selectedFilters); const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters); const processFilters = getMatchingFilters(values(ProcessTypeFilter), selectedFilters); const typeFilters = pipe( () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)), + set => groupFilters.length > 0 + ? set.add(ObjectTypeFilter.PROJECT) + : set, set => collectionFilters.length > 0 ? set.add(ObjectTypeFilter.COLLECTION) : set, @@ -182,6 +195,30 @@ const buildCollectionTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filter } }; +const serializeGroupTypeFilters = ({ fb, selectedFilters }: ReturnType) => pipe( + () => getMatchingFilters(values(GroupTypeFilter), selectedFilters), + filters => filters, + mappedFilters => ({ + fb: buildGroupTypeFilters({ fb, filters: mappedFilters, use_prefix: true }), + selectedFilters + }) +)(); + +const GROUP_TYPES = values(GroupTypeFilter); + +const buildGroupTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => { + switch (true) { + case filters.length === 0 || filters.length === GROUP_TYPES.length: + return fb; + case includes(GroupTypeFilter.PROJECT, filters): + return fb.addEqual('groups.group_class', 'project'); + case includes(GroupTypeFilter.FILTER_GROUP, filters): + return fb.addEqual('groups.group_class', 'filter'); + default: + return fb; + } +}; + const serializeProcessTypeFilters = ({ fb, selectedFilters }: ReturnType) => pipe( () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters), filters => filters, @@ -210,6 +247,7 @@ const buildProcessTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilde export const serializeResourceTypeFilters = pipe( createFiltersBuilder, serializeObjectTypeFilters, + serializeGroupTypeFilters, serializeCollectionTypeFilters, serializeProcessTypeFilters, ({ fb }) => fb.getFilters(), @@ -260,4 +298,4 @@ export const buildProcessStatusFilters = ( fb:FilterBuilder, activeStatusFilter: } } return fb; -}; \ No newline at end of file +}; diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts index ff506103..05d61927 100644 --- a/src/store/side-panel-tree/side-panel-tree-actions.ts +++ b/src/store/side-panel-tree/side-panel-tree-actions.ts @@ -112,7 +112,7 @@ const loadSharedRoot = async (dispatch: Dispatch, getState: () => RootState, ser const params = { filters: `[${new FilterBuilder() .addIsA('uuid', ResourceKind.PROJECT) - .addEqual('group_class', GroupClass.PROJECT) + .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER]) .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites') .getFilters()}]`, order: new OrderBuilder() 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 index fd328221..1932194c 100644 --- 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 @@ -2,11 +2,12 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { projectActionSet, readOnlyProjectActionSet } from "./project-action-set"; +import { filterGroupActionSet, 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), []); + const flattFilterGroupActionSet = filterGroupActionSet.reduce((prev, next) => prev.concat(next), []); describe('projectActionSet', () => { it('should not be empty', () => { @@ -33,4 +34,17 @@ describe('project-action-set', () => { .not.toEqual(expect.arrayContaining(flattProjectActionSet)); }) }); -}); \ No newline at end of file + + describe('filterGroupActionSet', () => { + it('should not be empty', () => { + // then + expect(flattFilterGroupActionSet.length).toBeGreaterThan(0); + }); + + it('should not contain projectActionSet items', () => { + // then + expect(flattFilterGroupActionSet) + .not.toEqual(expect.arrayContaining(flattProjectActionSet)); + }) + }); +}); 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 57ba0ea3..800f57d9 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 @@ -66,16 +66,9 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[ }, ]]; -export const projectActionSet: ContextMenuActionSet = [ +export const filterGroupActionSet: ContextMenuActionSet = [ [ ...readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []), - { - icon: NewProjectIcon, - name: "New project", - execute: (dispatch, resource) => { - dispatch(openProjectCreateDialog(resource.uuid)); - } - }, { icon: RenameIcon, name: "Edit project", @@ -106,3 +99,16 @@ export const projectActionSet: ContextMenuActionSet = [ }, ] ]; + +export const projectActionSet: ContextMenuActionSet = [ + [ + ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []), + { + icon: NewProjectIcon, + name: "New project", + execute: (dispatch, resource) => { + dispatch(openProjectCreateDialog(resource.uuid)); + } + }, + ] +]; diff --git a/src/views-components/context-menu/action-sets/project-admin-action-set.ts b/src/views-components/context-menu/action-sets/project-admin-action-set.ts index a3a8ce79..982a7883 100644 --- a/src/views-components/context-menu/action-sets/project-admin-action-set.ts +++ b/src/views-components/context-menu/action-sets/project-admin-action-set.ts @@ -7,7 +7,7 @@ import { TogglePublicFavoriteAction } from "~/views-components/context-menu/acti import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions"; import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action"; -import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set"; +import { projectActionSet, filterGroupActionSet } from "~/views-components/context-menu/action-sets/project-action-set"; export const projectAdminActionSet: ContextMenuActionSet = [[ ...projectActionSet.reduce((prev, next) => prev.concat(next), []), @@ -21,3 +21,16 @@ export const projectAdminActionSet: ContextMenuActionSet = [[ } } ]]; + +export const filterGroupAdminActionSet: ContextMenuActionSet = [[ + ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []), + { + component: TogglePublicFavoriteAction, + name: 'TogglePublicFavoriteAction', + execute: (dispatch, resource) => { + dispatch(togglePublicFavorite(resource)).then(() => { + dispatch(publicFavoritePanelActions.REQUEST_ITEMS()); + }); + } + } +]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 7f2a29f8..ee87d71a 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -67,8 +67,10 @@ export enum ContextMenuKind { API_CLIENT_AUTHORIZATION = "ApiClientAuthorization", ROOT_PROJECT = "RootProject", PROJECT = "Project", + FILTER_GROUP = "FilterGroup", READONLY_PROJECT = 'ReadOnlyProject', PROJECT_ADMIN = "ProjectAdmin", + FILTER_GROUP_ADMIN = "FilterGroupAdmin", RESOURCE = "Resource", FAVORITE = "Favorite", TRASH = "Trash", diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 28a6f253..93abb15e 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -468,16 +468,16 @@ export const ResourceOwnerWithName = ; }); -const renderType = (type: string) => +const renderType = (type: string, subtype: string) => - {resourceLabel(type)} + {resourceLabel(type, subtype)} ; export const ResourceType = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); - return { type: resource ? resource.kind : '' }; - })((props: { type: string }) => renderType(props.type)); + return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' }; + })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype)); export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => { return { resource: getResource(props.uuid)(state.resources) }; diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 47dbd9b0..35a7f9c1 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -44,6 +44,7 @@ import { getInitialProcessStatusFilters } from '~/store/resource-type-filters/resource-type-filters'; import { GroupContentsResource } from '~/services/groups-service/groups-service'; +import { GroupClass, GroupResource } from '~/models/group'; type CssRules = 'root' | "button"; @@ -167,7 +168,14 @@ export const ProjectPanel = withStyles(styles)( handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { const { resources } = this.props; const resource = getResource(resourceUuid)(resources); - const menuKind = this.props.dispatch(resourceUuidToContextMenuKind(resourceUuid)); + // When viewing the contents of a filter group, all contents should be treated as read only. + let readonly = false; + const project = getResource(this.props.currentItemId)(resources); + if (project && project.groupClass === GroupClass.FILTER) { + readonly = true; + } + + const menuKind = this.props.dispatch(resourceUuidToContextMenuKind(resourceUuid, readonly)); if (menuKind && resource) { this.props.dispatch(openContextMenu(event, { name: resource.name, -- 2.30.2