.and('not.be.disabled');
})
- it('disables or enables the +NEW side panel button on depending on project permissions', function() {
+ it('disables or enables the +NEW side panel button depending on project permissions', function() {
cy.loginAs(activeUser);
[true, false].map(function(isWritable) {
cy.createGroup(adminUser.token, {
.and('be.disabled');
})
})
-})
\ No newline at end of file
+
+ it('disables the +NEW side panel button when viewing filter group', function() {
+ cy.loginAs(adminUser);
+ cy.createGroup(adminUser.token, {
+ name: `my-favorite-filter-group`,
+ group_class: 'filter',
+ }).as('myFavoriteFilterGroup').then(function (myFavoriteFilterGroup) {
+ cy.contains('Refresh').click();
+ cy.doSearch(`${myFavoriteFilterGroup.uuid}`);
+ cy.get('[data-cy=breadcrumb-last]').should('contain', 'my-favorite-filter-group');
+
+ cy.get('[data-cy=side-panel-button]')
+ .should('exist')
+ .and(`be.disabled`);
+ })
+ })
+
+})
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";
import FlipToFront from '@material-ui/icons/FlipToFront';
import Folder from '@material-ui/icons/Folder';
import FolderShared from '@material-ui/icons/FolderShared';
+import Pageview from '@material-ui/icons/Pageview';
import GetApp from '@material-ui/icons/GetApp';
import Help from '@material-ui/icons/Help';
import HelpOutline from '@material-ui/icons/HelpOutline';
export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
+export const FilterGroupIcon: IconType = (props) => <Pageview {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
export const RemoveIcon: IconType = (props) => <Delete {...props} />;
import * as React from 'react';
import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core";
import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon } from '~/components/icon/icon';
+import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon } from '~/components/icon/icon';
import { ReactElement } from "react";
import CircularProgress from '@material-ui/core/CircularProgress';
import classnames from "classnames";
import { ArvadosTheme } from '~/common/custom-theme';
import { SidePanelRightArrowIcon } from '../icon/icon';
import { ResourceKind } from '~/models/resource';
+import { GroupClass } from '~/models/group';
type CssRules = 'list'
| 'listItem'
toggleActive: 'TOGGLE_ACTIVE',
};
-const ItemIcon = React.memo(({type, kind, active, classes}: any) => {
+const ItemIcon = React.memo(({type, kind, active, groupClass, classes}: any) => {
let Icon = ProjectIcon;
- if (type) {
- switch (type) {
- case 'directory':
- Icon = DirectoryIcon;
- break;
- case 'file':
- Icon = FileIcon;
- break;
- default:
- Icon = DefaultIcon;
- }
+ if (groupClass === GroupClass.FILTER) {
+ Icon = FilterGroupIcon;
+ }
+
+ if (type) {
+ switch (type) {
+ case 'directory':
+ Icon = DirectoryIcon;
+ break;
+ case 'file':
+ Icon = FileIcon;
+ break;
+ default:
+ Icon = DefaultIcon;
}
+ }
- if (kind) {
- switch(kind) {
- case ResourceKind.COLLECTION:
- Icon = CollectionIcon;
- break;
- default:
- break;
- }
+ if (kind) {
+ switch(kind) {
+ case ResourceKind.COLLECTION:
+ Icon = CollectionIcon;
+ break;
+ default:
+ break;
}
+ }
return <Icon className={classnames({ [classes.active]: active }, classes.childItemIcon)} />;
});
</i>
<div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer}>
<span style={{ display: 'flex', alignItems: 'center' }}>
- <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} classes={props.classes} />
+ <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
<span style={{ fontSize: '0.875rem' }}>
{item.data.name}
</span>
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';
import { Config } from '~/common/config';
import { addRouteChangeHandlers } from './routes/route-change-handlers';
import { setTokenDialogApiHost } from '~/store/token-dialog/token-dialog-actions';
-import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
+import { processResourceActionSet, readOnlyProcessResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
import { setBuildInfo } from '~/store/app-info/app-info-actions';
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';
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);
addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
+addMenuActionSet(ContextMenuKind.READONLY_PROCESS_RESOURCE, readOnlyProcessResourceActionSet);
addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
+addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
storeRedirects();
}
export enum GroupClass {
- PROJECT = "project"
+ PROJECT = 'project',
+ FILTER = 'filter',
}
import { GroupClass, GroupResource } from "./group";
export interface ProjectResource extends GroupResource {
- groupClass: GroupClass.PROJECT;
+ groupClass: GroupClass.PROJECT | GroupClass.FILTER;
}
export const getProjectUrl = (uuid: string) => {
expect(axiosInstance.get).toHaveBeenCalledWith("/groups", {
params: {
filters: "[" + new FilterBuilder()
- .addEqual("group_class", "project")
+ .addIn("group_class", ["project", "filter"])
.getFilters() + "]",
order: undefined
}
filters: joinFilters(
args.filters || '',
new FilterBuilder()
- .addEqual("group_class", GroupClass.PROJECT)
+ .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
.getFilters()
)
});
import { resourceUuidToContextMenuKind } from './context-menu-actions';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
+import { PROJECT_PANEL_CURRENT_UUID } from '../project-panel/project-panel-action';
+import { GroupClass } from '~/models/group';
describe('context-menu-actions', () => {
describe('resourceUuidToContextMenuKind', () => {
const headCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaaa';
const oldCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaab';
const projectUuid = 'zzzzz-j7d0g-ccccccccccccccc';
+ const filterGroupUuid = 'zzzzz-j7d0g-ccccccccccccccd';
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],
+ // resourceUuid, isAdminUser, isEditable, isTrashed, forceReadonly, expected
+ [headCollectionUuid, false, true, true, false, ContextMenuKind.TRASHED_COLLECTION],
+ [headCollectionUuid, false, true, false, false, ContextMenuKind.COLLECTION],
+ [headCollectionUuid, false, true, false, true, ContextMenuKind.READONLY_COLLECTION],
+ [headCollectionUuid, false, false, true, false, ContextMenuKind.READONLY_COLLECTION],
+ [headCollectionUuid, false, false, false, false, ContextMenuKind.READONLY_COLLECTION],
+ [headCollectionUuid, true, true, true, false, ContextMenuKind.TRASHED_COLLECTION],
+ [headCollectionUuid, true, true, false, false, ContextMenuKind.COLLECTION_ADMIN],
+ [headCollectionUuid, true, false, true, false, ContextMenuKind.TRASHED_COLLECTION],
+ [headCollectionUuid, true, false, false, false, ContextMenuKind.COLLECTION_ADMIN],
+ [headCollectionUuid, true, false, false, true, ContextMenuKind.READONLY_COLLECTION],
- [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],
+ [oldCollectionUuid, false, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, false, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, false, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, false, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, true, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, true, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, true, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+ [oldCollectionUuid, true, false, 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],
+ // [projectUuid, false, true, true, false, ContextMenuKind.TRASHED_PROJECT],
+ [projectUuid, false, true, false, false, ContextMenuKind.PROJECT],
+ [projectUuid, false, true, false, true, ContextMenuKind.READONLY_PROJECT],
+ [projectUuid, false, false, true, false, ContextMenuKind.READONLY_PROJECT],
+ [projectUuid, false, false, false, false, ContextMenuKind.READONLY_PROJECT],
+ // [projectUuid, true, true, true, false, ContextMenuKind.TRASHED_PROJECT],
+ [projectUuid, true, true, false, false, ContextMenuKind.PROJECT_ADMIN],
+ // [projectUuid, true, false, true, false, ContextMenuKind.TRASHED_PROJECT],
+ [projectUuid, true, false, false, false, ContextMenuKind.PROJECT_ADMIN],
+ [projectUuid, true, false, false, true, ContextMenuKind.READONLY_PROJECT],
- [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],
+ [linkUuid, false, true, true, false, ContextMenuKind.LINK],
+ [linkUuid, false, true, false, false, ContextMenuKind.LINK],
+ [linkUuid, false, false, true, false, ContextMenuKind.LINK],
+ [linkUuid, false, false, false, false, ContextMenuKind.LINK],
+ [linkUuid, true, true, true, false, ContextMenuKind.LINK],
+ [linkUuid, true, true, false, false, ContextMenuKind.LINK],
+ [linkUuid, true, false, true, false, ContextMenuKind.LINK],
+ [linkUuid, true, false, 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],
+ [userUuid, false, true, true, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, false, true, false, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, false, false, true, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, false, false, false, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, true, true, true, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, true, true, false, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, true, false, true, false, ContextMenuKind.ROOT_PROJECT],
+ [userUuid, true, false, 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],
+ [containerRequestUuid, false, true, true, false, ContextMenuKind.PROCESS_RESOURCE],
+ [containerRequestUuid, false, true, false, false, ContextMenuKind.PROCESS_RESOURCE],
+ [containerRequestUuid, false, false, true, false, ContextMenuKind.PROCESS_RESOURCE],
+ [containerRequestUuid, false, false, false, false, ContextMenuKind.PROCESS_RESOURCE],
+ [containerRequestUuid, false, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE],
+ [containerRequestUuid, true, true, true, false, ContextMenuKind.PROCESS_ADMIN],
+ [containerRequestUuid, true, true, false, false, ContextMenuKind.PROCESS_ADMIN],
+ [containerRequestUuid, true, false, true, false, ContextMenuKind.PROCESS_ADMIN],
+ [containerRequestUuid, true, false, false, false, ContextMenuKind.PROCESS_ADMIN],
+ [containerRequestUuid, true, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE],
]
- cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, expected]) => {
+ cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, forceReadonly, expected]) => {
const initialState = {
+ properties: {
+ [PROJECT_PANEL_CURRENT_UUID]: projectUuid,
+ },
resources: {
[headCollectionUuid]: {
uuid: headCollectionUuid,
uuid: oldCollectionUuid,
currentVersionUuid: headCollectionUuid,
isTrashed: isTrashed,
-
},
[projectUuid]: {
uuid: projectUuid,
ownerUuid: isEditable ? userUuid : otherUserUuid,
writableBy: isEditable ? [userUuid] : [otherUserUuid],
+ groupClass: GroupClass.PROJECT,
+ },
+ [filterGroupUuid]: {
+ uuid: filterGroupUuid,
+ ownerUuid: isEditable ? userUuid : otherUserUuid,
+ writableBy: isEditable ? [userUuid] : [otherUserUuid],
+ groupClass: GroupClass.FILTER,
},
[linkUuid]: {
uuid: linkUuid,
};
const store = mockStore(initialState);
- const menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string))
+ let menuKind: any;
try {
+ menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string, forceReadonly 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])} forceReadonly: ${forceReadonly} expected to be ${expected} but got ${menuKind}.`);
}
});
});
});
-});
\ No newline at end of file
+});
import { KeepServiceResource } from '~/models/keep-services';
import { ProcessResource } from '~/models/process';
import { CollectionResource } from '~/models/collection';
-import { GroupResource } from '~/models/group';
+import { GroupClass, GroupResource } from '~/models/group';
import { GroupContentsResource } from '~/services/groups-service/groups-service';
export const contextMenuActions = unionize({
}
};
-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<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
- const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
+
+ const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
switch (kind) {
case ResourceKind.PROJECT:
- return !isAdminUser
- ? isEditable
- ? ContextMenuKind.PROJECT
- : ContextMenuKind.READONLY_PROJECT
- : ContextMenuKind.PROJECT_ADMIN;
+ return (isAdminUser && !readonly)
+ ? (resource && resource.groupClass !== GroupClass.FILTER)
+ ? ContextMenuKind.PROJECT_ADMIN
+ : ContextMenuKind.FILTER_GROUP_ADMIN
+ : isEditable
+ ? (resource && resource.groupClass !== GroupClass.FILTER)
+ ? ContextMenuKind.PROJECT
+ : ContextMenuKind.FILTER_GROUP
+ : ContextMenuKind.READONLY_PROJECT;
case ResourceKind.COLLECTION:
const c = getResource<CollectionResource>(uuid)(getState().resources);
if (c === undefined) { return; }
? ContextMenuKind.OLD_VERSION_COLLECTION
: (isTrashed && isEditable)
? ContextMenuKind.TRASHED_COLLECTION
- : isAdminUser
+ : (isAdminUser && !readonly)
? ContextMenuKind.COLLECTION_ADMIN
: isEditable
? ContextMenuKind.COLLECTION
: ContextMenuKind.READONLY_COLLECTION;
case ResourceKind.PROCESS:
- return !isAdminUser
- ? ContextMenuKind.PROCESS_RESOURCE
- : ContextMenuKind.PROCESS_ADMIN;
+ return (isAdminUser && !readonly)
+ ? ContextMenuKind.PROCESS_ADMIN
+ : readonly
+ ? ContextMenuKind.READONLY_PROCESS_RESOURCE
+ : ContextMenuKind.PROCESS_RESOURCE;
case ResourceKind.USER:
return ContextMenuKind.ROOT_PROJECT;
case ResourceKind.LINK:
order.addOrder(direction, 'name');
}
const filters = new FilterBuilder()
- .addNotIn('group_class', [GroupClass.PROJECT])
+ .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
.addILike('name', dataExplorer.searchValue)
.getFilters();
const response = await this.services.groupsService
//
// 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';
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"]`);
+ });
+
});
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 {
// causing compile issues.
export const getInitialResourceTypeFilters = pipe(
(): DataTableFilters => createTree<DataTableFilterItem>(),
- 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),
};
const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
+ 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,
}
};
+const serializeGroupTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => 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<typeof createFiltersBuilder>) => pipe(
() => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
filters => filters,
export const serializeResourceTypeFilters = pipe(
createFiltersBuilder,
serializeObjectTypeFilters,
+ serializeGroupTypeFilters,
serializeCollectionTypeFilters,
serializeProcessTypeFilters,
({ fb }) => fb.getFilters(),
}
}
return fb;
-};
\ No newline at end of file
+};
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<ProjectResource>()
import { LinkResource, LinkClass } from "~/models/link";
import { mapTreeValues } from "~/models/tree";
import { sortFilesTree } from "~/services/collection-service/collection-service-files-response";
-import { GroupResource } from "~/models/group";
+import { GroupClass, GroupResource } from "~/models/group";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
pickerId: string;
includeCollections?: boolean;
includeFiles?: boolean;
+ includeFilterGroups?: boolean;
loadShared?: boolean;
}
export const loadProject = (params: LoadProjectParams) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
- const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
+ const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false } = params;
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
dispatch<any>(receiveTreePickerData<GroupContentsResource>({
id,
pickerId,
- data: items,
+ data: items.filter((item) => {
+ if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+ return false;
+ }
+ return true;
+ }),
extractNodeData: item => ({
id: item.uuid,
value: item,
import { openRemoveProcessDialog } from "~/store/processes/processes-actions";
import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
-export const processResourceActionSet: ContextMenuActionSet = [[
- {
- icon: RenameIcon,
- name: "Edit process",
- execute: (dispatch, resource) => {
- dispatch<any>(openProcessUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
+export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
});
}
},
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => {
- dispatch<any>(openMoveProcessDialog(resource));
- }
- },
{
icon: CopyIcon,
name: "Copy to project",
dispatch<any>(toggleDetailsPanel());
}
},
+]];
+
+export const processResourceActionSet: ContextMenuActionSet = [[
+ ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ icon: RenameIcon,
+ name: "Edit process",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openProcessUpdateDialog(resource));
+ }
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
+ }
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openMoveProcessDialog(resource));
+ }
+ },
{
name: "Remove",
icon: RemoveIcon,
//
// 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', () => {
.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));
+ })
+ });
+});
},
]];
-export const projectActionSet: ContextMenuActionSet = [
+export const filterGroupActionSet: 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",
},
]
];
+
+export const projectActionSet: ContextMenuActionSet = [
+ [
+ ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ icon: NewProjectIcon,
+ name: "New project",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openProjectCreateDialog(resource.uuid));
+ }
+ },
+ ]
+];
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), []),
}
}
]];
+
+export const filterGroupAdminActionSet: ContextMenuActionSet = [[
+ ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ component: TogglePublicFavoriteAction,
+ name: 'TogglePublicFavoriteAction',
+ execute: (dispatch, resource) => {
+ dispatch<any>(togglePublicFavorite(resource)).then(() => {
+ dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+ }
+]];
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",
PROCESS = "Process",
PROCESS_ADMIN = 'ProcessAdmin',
PROCESS_RESOURCE = 'ProcessResource',
+ READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
PROCESS_LOGS = "ProcessLogs",
REPOSITORY = "Repository",
SSH_KEY = "SshKey",
import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
import { ResourceKind, TrashableResource } from '~/models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
+import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize, formatTime } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
import { connect, DispatchProp } from 'react-redux';
import { CollectionResource } from '~/models/collection';
import { IllegalNamingWarning } from '~/components/warning/warning';
import { loadResource } from '~/store/resources/resources-actions';
+import { GroupClass } from '~/models/group';
const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
const renderIcon = (item: GroupContentsResource) => {
switch (item.kind) {
case ResourceKind.PROJECT:
+ if (item.groupClass === GroupClass.FILTER) {
+ return <FilterGroupIcon />;
+ }
return <ProjectIcon />;
case ResourceKind.COLLECTION:
if (item.uuid === item.currentVersionUuid) {
</Typography>;
});
-const renderType = (type: string) =>
+const renderType = (type: string, subtype: string) =>
<Typography noWrap>
- {resourceLabel(type)}
+ {resourceLabel(type, subtype)}
</Typography>;
export const ResourceType = connect(
(state: RootState, props: { uuid: string }) => {
const resource = getResource<GroupContentsResource>(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<GroupContentsResource>(props.uuid)(state.resources) };
import * as React from 'react';
import { connect } from 'react-redux';
import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
-import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
+import { ProjectIcon, RenameIcon, FilterGroupIcon } from '~/components/icon/icon';
import { ProjectResource } from '~/models/project';
import { formatDate } from '~/common/formatters';
import { ResourceKind } from '~/models/resource';
import { Dispatch } from 'redux';
import { getPropertyChip } from '../resource-properties-form/property-chip';
import { ResourceOwnerWithName } from '../data-explorer/renderers';
+import { GroupClass } from "~/models/group";
export class ProjectDetails extends DetailsData<ProjectResource> {
getIcon(className?: string) {
+ if (this.item.groupClass === GroupClass.FILTER) {
+ return <FilterGroupIcon className={className} />;
+ }
return <ProjectIcon className={className} />;
}
const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
withStyles(styles)(
({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
- <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
+ <DetailsAttribute label='Type' value={project.groupClass === GroupClass.FILTER ? 'Filter group' : resourceLabel(ResourceKind.PROJECT)} />
<DetailsAttribute label='Owner' linkToUuid={project.ownerUuid}
uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
<DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
}
</DetailsAttribute>
<DetailsAttribute label='Properties'>
- <div onClick={onClick}>
- <RenameIcon className={classes.editIcon} />
- </div>
+ {project.groupClass !== GroupClass.FILTER ?
+ <div onClick={onClick}>
+ <RenameIcon className={classes.editIcon} />
+ </div>
+ : ''
+ }
</DetailsAttribute>
{
Object.keys(project.properties).map(k =>
const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
const filterGroups = new FilterBuilder()
- .addNotIn('group_class', [GroupClass.PROJECT])
+ .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
.addILike('name', value)
.getFilters();
import { runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
import { getUserUuid } from '~/common/getuser';
import { matchProjectRoute } from '~/routes/routes';
-import { GroupResource } from '~/models/group';
+import { GroupClass, GroupResource } from '~/models/group';
import { ResourcesState, getResource } from '~/store/resources/resources';
import { extractUuidKind, ResourceKind } from '~/models/resource';
const currentProject = getResource<GroupResource>(currentItemId)(resources);
if (currentProject &&
currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
- !isProjectTrashed(currentProject, resources)) {
+ !isProjectTrashed(currentProject, resources) &&
+ currentProject.groupClass !== GroupClass.FILTER) {
enabled = true;
}
}
}
}
)
-);
\ No newline at end of file
+);
import { TreeItem } from "~/components/tree/tree";
import { ProjectResource } from "~/models/project";
import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
-import { ProcessIcon, ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon } from '~/components/icon/icon';
+import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon } from '~/components/icon/icon';
import { WorkflowIcon } from '~/components/icon/icon';
import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
import { noop } from 'lodash';
import { ResourceKind } from "~/models/resource";
import { IllegalNamingWarning } from "~/components/warning/warning";
+import { GroupClass } from "~/models/group";
+
export interface SidePanelTreeProps {
onItemActivation: (id: string) => void;
sidePanelProgress?: boolean;
const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
typeof item.data === 'string'
? getSidePanelIcon(item.data)
- : ProjectIcon;
+ : (item.data && item.data.groupClass === GroupClass.FILTER)
+ ? FilterGroupIcon
+ : ProjectIcon;
const getSidePanelIcon = (category: string) => {
switch (category) {
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";
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
const { resources } = this.props;
const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
- const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
+ // When viewing the contents of a filter group, all contents should be treated as read only.
+ let readonly = false;
+ const project = getResource<GroupResource>(this.props.currentItemId)(resources);
+ if (project && project.groupClass === GroupClass.FILTER) {
+ readonly = true;
+ }
+
+ const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
if (menuKind && resource) {
this.props.dispatch<any>(openContextMenu(event, {
name: resource.name,