74179/PR#925: Groups with linked dso (workflow groups) => No edit/delete possible + alert on edit page with link to edit roles on comcol page

This commit is contained in:
Marie Verdonck
2020-11-17 14:48:10 +01:00
parent 1fcd4d6c3e
commit dfd7468b88
9 changed files with 179 additions and 30 deletions

View File

@@ -13,12 +13,12 @@ import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
{ {
path: `${GROUP_EDIT_PATH}/:groupId`, path: `${GROUP_EDIT_PATH}/:groupId`,
component: GroupFormComponent, component: GroupFormComponent,
data: {title: 'admin.registries.schema.title'} data: {title: 'admin.access-control.groups.title.singleGroup'}
}, },
{ {
path: `${GROUP_EDIT_PATH}/newGroup`, path: `${GROUP_EDIT_PATH}/newGroup`,
component: GroupFormComponent, component: GroupFormComponent,
data: {title: 'admin.registries.schema.title'} data: {title: 'admin.access-control.groups.title.addGroup'}
}, },
]) ])
] ]

View File

@@ -1,6 +1,11 @@
<div class="container"> <div class="container">
<div class="group-form row"> <div class="group-form row">
<div class="col-12"> <div class="col-12">
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
<ds-alert *ngIf="!(canEdit$ | async)" [type]="AlertTypeEnum.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: (getLinkedDSO(groupBeingEdited) | async)?.payload?.name, comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
</ds-alert>
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div> <div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
@@ -19,14 +24,17 @@
(cancel)="onCancel()" (cancel)="onCancel()"
(submitForm)="onSubmit()"> (submitForm)="onSubmit()">
<div *ngIf="groupBeingEdited != null" class="row"> <div *ngIf="groupBeingEdited != null" class="row">
<button class="btn btn-light delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()"> <button class="btn btn-light delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
(click)="delete()">
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}} <i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
</button> </button>
</div> </div>
</ds-form> </ds-form>
<ds-members-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list> <ds-members-list *ngIf="groupBeingEdited != null"
<ds-subgroups-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list> [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
<ds-subgroups-list *ngIf="groupBeingEdited != null"
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
<div> <div>
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]" <button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"

View File

@@ -12,20 +12,30 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { combineLatest } from 'rxjs/internal/observable/combineLatest'; import { combineLatest } from 'rxjs/internal/observable/combineLatest';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
import { switchMap, take } from 'rxjs/operators'; import { ObservedValueOf } from 'rxjs/internal/types';
import { map, switchMap, take } from 'rxjs/operators';
import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths';
import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths';
import { RestResponse } from '../../../../core/cache/response.models'; import { RestResponse } from '../../../../core/cache/response.models';
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../../core/data/request.service';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { AlertType } from '../../../../shared/alert/aletr-type';
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-group-form', selector: 'ds-group-form',
@@ -98,10 +108,17 @@ export class GroupFormComponent implements OnInit, OnDestroy {
/** /**
* Observable whether or not the logged in user is allowed to delete the Group * Observable whether or not the logged in user is allowed to delete the Group
*/ */
canDelete$: Observable<boolean>; canEdit$: Observable<boolean>;
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;
constructor(public groupDataService: GroupDataService, constructor(public groupDataService: GroupDataService,
private ePersonDataService: EPersonDataService, private ePersonDataService: EPersonDataService,
private dSpaceObjectDataService: DSpaceObjectDataService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@@ -120,12 +137,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.subs.push(this.route.params.subscribe((params) => { this.subs.push(this.route.params.subscribe((params) => {
this.setActiveGroup(params.groupId) this.setActiveGroup(params.groupId)
})); }));
this.canDelete$ = this.groupDataService.getActiveGroup().pipe( this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined)) switchMap((group: Group) => {
return combineLatest(
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
this.hasLinkedDSO(group),
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
return isAuthorized && !hasLinkedDSO;
})
})
); );
combineLatest( combineLatest(
this.translateService.get(`${this.messagePrefix}.groupName`), this.translateService.get(`${this.messagePrefix}.groupName`),
this.translateService.get(`${this.messagePrefix}.groupDescription`), this.translateService.get(`${this.messagePrefix}.groupDescription`)
).subscribe(([groupName, groupDescription]) => { ).subscribe(([groupName, groupDescription]) => {
this.groupName = new DynamicInputModel({ this.groupName = new DynamicInputModel({
id: 'groupName', id: 'groupName',
@@ -147,18 +171,23 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDescription, this.groupDescription,
]; ];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { this.subs.push(
combineLatest(
this.groupDataService.getActiveGroup(),
this.canEdit$
).subscribe(([activeGroup, canEdit]) => {
if (activeGroup != null) { if (activeGroup != null) {
this.groupBeingEdited = activeGroup; this.groupBeingEdited = activeGroup;
this.formGroup.patchValue({ this.formGroup.patchValue({
groupName: activeGroup != null ? activeGroup.name : '', groupName: activeGroup != null ? activeGroup.name : '',
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '', groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
}); });
if (activeGroup.permanent) { if (!canEdit || activeGroup.permanent) {
this.formGroup.get('groupName').disable(); this.formGroup.disable();
} }
} }
})); })
);
}); });
} }
@@ -298,7 +327,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup === null) { if (activeGroup === null) {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.groupDataService.findByHref(groupSelfLink) this.groupDataService.findByHref(groupSelfLink, followLink('subgroups'), followLink('epersons'), followLink('object'))
.pipe( .pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload()) getRemoteDataPayload())
@@ -335,7 +364,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: optionalErrorMessage })); this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: optionalErrorMessage }));
} }
}) })
}} }
}
}); });
}) })
} }
@@ -358,4 +388,57 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.onCancel(); this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
/**
* Check if group has a linked object (community or collection linked to a workflow group)
* @param group
*/
hasLinkedDSO(group: Group): Observable<boolean> {
if (hasValue(group) && hasValue(group._links.object.href)) {
return this.getLinkedDSO(group).pipe(
map((rd: RemoteData<DSpaceObject>) => {
if (hasValue(rd) && hasValue(rd.payload)) {
return true;
} else {
return false
}
})
);
}
}
/**
* Get group's linked object if it has one (community or collection linked to a workflow group)
* @param group
*/
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
if (hasValue(group) && hasValue(group._links.object.href)) {
if (group.object == undefined) {
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
}
return group.object;
}
}
/**
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
* @param group
*/
getLinkedEditRolesRoute(group: Group): Observable<String> {
if (hasValue(group) && hasValue(group._links.object.href)) {
return this.getLinkedDSO(group).pipe(
map((rd: RemoteData<DSpaceObject>) => {
if (hasValue(rd) && hasValue(rd.payload)) {
const dso = rd.payload
switch ((dso as any).type) {
case Community.type.value:
return getCommunityEditRolesRoute(rd.payload.id);
case Collection.type.value:
return getCollectionEditRolesRoute(rd.payload.id);
}
}
})
)
}
}
} }

View File

@@ -5,7 +5,9 @@ import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { filter } from 'rxjs/internal/operators/filter'; import { filter } from 'rxjs/internal/operators/filter';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
import { ObservedValueOf } from 'rxjs/internal/types';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
@@ -17,6 +19,7 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model'; import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../../core/services/route.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
@@ -72,6 +75,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
constructor(public groupService: GroupDataService, constructor(public groupService: GroupDataService,
private ePersonDataService: EPersonDataService, private ePersonDataService: EPersonDataService,
private dSpaceObjectDataService: DSpaceObjectDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@@ -120,16 +124,18 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
this.subs.push(this.groups$.pipe( this.subs.push(this.groups$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
switchMap((groups) => { switchMap((groups: PaginatedList<Group>) => {
return combineLatest(...groups.page.map((group) => { return combineLatest(...groups.page.map((group: Group) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined).pipe( return combineLatest(
map((authorized: boolean) => { this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
this.hasLinkedDSO(group),
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
const groupDtoModel: GroupDtoModel = new GroupDtoModel(); const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = authorized; groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
groupDtoModel.group = group; groupDtoModel.group = group;
return groupDtoModel; return groupDtoModel;
}) }
); )
})).pipe(map((dtos: GroupDtoModel[]) => { })).pipe(map((dtos: GroupDtoModel[]) => {
return new PaginatedList(groups.pageInfo, dtos); return new PaginatedList(groups.pageInfo, dtos);
})) }))
@@ -188,6 +194,25 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
return this.groupService.findAllByHref(group._links.subgroups.href); return this.groupService.findAllByHref(group._links.subgroups.href);
} }
/**
* Check if group has a linked object (community or collection linked to a workflow group)
* @param group
*/
hasLinkedDSO(group: Group): Observable<boolean> {
if (group.object == undefined) {
group.object = this.dSpaceObjectDataService.findByHref(group._links.object.href);
}
return group.object.pipe(
map((rd: RemoteData<DSpaceObject>) => {
if (hasValue(rd) && hasValue(rd.payload)) {
return true;
} else {
return false
}
})
);
}
/** /**
* Reset all input-fields to be empty and search all search * Reset all input-fields to be empty and search all search
*/ */

View File

@@ -20,6 +20,11 @@ export function getCollectionCreateRoute() {
return new URLCombiner(getCollectionModuleRoute(), COLLECTION_CREATE_PATH).toString() return new URLCombiner(getCollectionModuleRoute(), COLLECTION_CREATE_PATH).toString()
} }
export function getCollectionEditRolesRoute(id) {
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString()
}
export const COLLECTION_CREATE_PATH = 'create'; export const COLLECTION_CREATE_PATH = 'create';
export const COLLECTION_EDIT_PATH = 'edit'; export const COLLECTION_EDIT_PATH = 'edit';
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
export const ITEMTEMPLATE_PATH = 'itemtemplate'; export const ITEMTEMPLATE_PATH = 'itemtemplate';

View File

@@ -1,3 +1,4 @@
import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
export const COMMUNITY_PARENT_PARAMETER = 'parent'; export const COMMUNITY_PARENT_PARAMETER = 'parent';
@@ -20,5 +21,10 @@ export function getCommunityCreateRoute() {
return new URLCombiner(getCommunityModuleRoute(), COMMUNITY_CREATE_PATH).toString() return new URLCombiner(getCommunityModuleRoute(), COMMUNITY_CREATE_PATH).toString()
} }
export function getCommunityEditRolesRoute(id) {
return new URLCombiner(getCollectionPageRoute(id), COMMUNITY_EDIT_PATH, COMMUNITY_EDIT_ROLES_PATH).toString()
}
export const COMMUNITY_CREATE_PATH = 'create'; export const COMMUNITY_CREATE_PATH = 'create';
export const COMMUNITY_EDIT_PATH = 'edit'; export const COMMUNITY_EDIT_PATH = 'edit';
export const COMMUNITY_EDIT_ROLES_PATH = 'roles';

View File

@@ -58,4 +58,8 @@ export class DSpaceObjectDataService {
findById(uuid: string): Observable<RemoteData<DSpaceObject>> { findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findById(uuid); return this.dataService.findById(uuid);
} }
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
return this.dataService.findByHref(href);
}
} }

View File

@@ -5,6 +5,7 @@ import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { DSPACE_OBJECT } from '../../shared/dspace-object.resource-type';
import { HALLink } from '../../shared/hal-link.model'; import { HALLink } from '../../shared/hal-link.model';
import { EPerson } from './eperson.model'; import { EPerson } from './eperson.model';
import { EPERSON } from './eperson.resource-type'; import { EPERSON } from './eperson.resource-type';
@@ -41,6 +42,7 @@ export class Group extends DSpaceObject {
self: HALLink; self: HALLink;
subgroups: HALLink; subgroups: HALLink;
epersons: HALLink; epersons: HALLink;
object: HALLink;
}; };
/** /**
@@ -57,4 +59,11 @@ export class Group extends DSpaceObject {
@link(EPERSON, true) @link(EPERSON, true)
public epersons?: Observable<RemoteData<PaginatedList<EPerson>>>; public epersons?: Observable<RemoteData<PaginatedList<EPerson>>>;
/**
* Connected dspace object, the community or collection connected to a workflow group (204 no content for non-workflow groups)
* Will be undefined unless the object {@link HALLink} has been resolved (can only be resolved for workflow groups)
*/
@link(DSPACE_OBJECT)
public object?: Observable<RemoteData<DSpaceObject>>;
} }

View File

@@ -280,6 +280,10 @@
"admin.access-control.groups.title": "DSpace Angular :: Groups", "admin.access-control.groups.title": "DSpace Angular :: Groups",
"admin.access-control.groups.title.singleGroup": "DSpace Angular :: Edit Group",
"admin.access-control.groups.title.addGroup": "DSpace Angular :: New Group",
"admin.access-control.groups.head": "Groups", "admin.access-control.groups.head": "Groups",
"admin.access-control.groups.button.add": "Add group", "admin.access-control.groups.button.add": "Add group",
@@ -313,6 +317,11 @@
"admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"", "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"",
"admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.",
"admin.access-control.groups.form.alert.workflowGroup": "This group cant be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the <a href='{{comcolEditRolesRoute}}'>\"assign roles\"</a> tab on the edit {{comcol}} page. You can still add and remove group members using this page.",
"admin.access-control.groups.form.head.create": "Create group", "admin.access-control.groups.form.head.create": "Create group",
"admin.access-control.groups.form.head.edit": "Edit group", "admin.access-control.groups.form.head.edit": "Edit group",