Files
dspace-angular/src/app/access-control/group-registry/group-form/group-form.component.ts
2025-05-23 13:56:46 +02:00

505 lines
18 KiB
TypeScript

import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, AbstractControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicFormControlModel,
DynamicFormLayout,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import {
combineLatest as observableCombineLatest,
Observable,
Subscription,
} from 'rxjs';
import { map, switchMap, take, debounceTime } from 'rxjs/operators';
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
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 {
getAllCompletedRemoteData,
getRemoteDataPayload,
getFirstSucceededRemoteData,
getFirstCompletedRemoteData,
} from '../../../core/shared/operators';
import { AlertType } from '../../../shared/alert/alert-type';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { environment } from '../../../../environments/environment';
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
import { RouteService } from '../../../core/services/route.service';
@Component({
selector: 'ds-group-form',
templateUrl: './group-form.component.html'
})
/**
* A form used for creating and editing groups
*/
export class GroupFormComponent implements OnInit, OnDestroy {
messagePrefix = 'admin.access-control.groups.form';
/**
* A unique id used for ds-form
*/
formId = 'group-form';
/**
* Dynamic models for the inputs of form
*/
groupName: AbstractControl;
groupCommunity: AbstractControl;
groupDescription: AbstractControl;
/**
* A list of all dynamic input models
*/
formModel: DynamicFormControlModel[];
/**
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
groupName: {
grid: {
host: 'row'
}
},
groupDescription: {
grid: {
host: 'row'
}
},
};
/**
* A FormGroup that combines all inputs
*/
formGroup: UntypedFormGroup;
/**
* An EventEmitter that's fired whenever the form is being submitted
*/
@Output() submitForm: EventEmitter<any> = new EventEmitter();
/**
* An EventEmitter that's fired whenever the form is cancelled
*/
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
/**
* List of subscriptions
*/
subs: Subscription[] = [];
/**
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
*/
canEdit$: Observable<boolean>;
/**
* The current {@link Group}
*/
activeGroup$: Observable<Group>;
/**
* The current {@link Group}'s linked {@link Community}/{@link Collection}
*/
activeGroupLinkedDSO$: Observable<DSpaceObject>;
/**
* Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab
*/
linkedEditRolesRoute$: Observable<string>;
/**
* The AlertType enumeration
*/
public readonly AlertType = AlertType;
/**
* Subscription to email field value change
*/
groupNameValueChangeSubscribe: Subscription;
constructor(
public groupDataService: GroupDataService,
protected dSpaceObjectDataService: DSpaceObjectDataService,
protected formBuilderService: FormBuilderService,
protected translateService: TranslateService,
protected notificationsService: NotificationsService,
protected route: ActivatedRoute,
protected router: Router,
protected authorizationService: AuthorizationDataService,
protected modalService: NgbModal,
public requestService: RequestService,
protected changeDetectorRef: ChangeDetectorRef,
public dsoNameService: DSONameService,
protected routeService: RouteService,
) {
}
ngOnInit(): void {
if (this.route.snapshot.params.groupId !== 'newGroup') {
this.setActiveGroup(this.route.snapshot.params.groupId);
}
this.activeGroup$ = this.groupDataService.getActiveGroup();
this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO();
this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute();
this.canEdit$ = this.activeGroupLinkedDSO$.pipe(
switchMap((dso: DSpaceObject) => {
if (hasValue(dso)) {
return [false];
} else {
return this.activeGroup$.pipe(
hasValueOperator(),
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)),
);
}
}),
);
this.initialisePage();
}
initialisePage() {
const groupNameModel = new DynamicInputModel({
id: 'groupName',
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
name: 'groupName',
validators: {
required: null,
},
required: true,
});
const groupCommunityModel = new DynamicInputModel({
id: 'groupCommunity',
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
name: 'groupCommunity',
required: false,
readOnly: true,
});
const groupDescriptionModel = new DynamicTextAreaModel({
id: 'groupDescription',
label: this.translateService.instant(`${this.messagePrefix}.groupDescription`),
name: 'groupDescription',
required: false,
spellCheck: environment.form.spellCheck,
});
this.formModel = [
groupNameModel,
groupDescriptionModel,
];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.groupName = this.formGroup.get('groupName');
this.groupDescription = this.formGroup.get('groupDescription');
if (hasValue(this.groupName)) {
this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
this.changeDetectorRef.detectChanges();
});
}
this.subs.push(
observableCombineLatest([
this.activeGroup$,
this.canEdit$,
this.activeGroupLinkedDSO$,
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
if (activeGroup != null) {
// Disable group name exists validator
this.formGroup.controls.groupName.clearAsyncValidators();
if (isNotEmpty(linkedObject?.name)) {
if (!this.formGroup.controls.groupCommunity) {
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel);
this.groupDescription = this.formGroup.get('groupCommunity');
}
this.formGroup.patchValue({
groupName: activeGroup.name,
groupCommunity: linkedObject?.name ?? '',
groupDescription: activeGroup.firstMetadataValue('dc.description'),
});
} else {
this.formModel = [
groupNameModel,
groupDescriptionModel,
];
this.formGroup.patchValue({
groupName: activeGroup.name,
groupDescription: activeGroup.firstMetadataValue('dc.description'),
});
}
if (!canEdit || activeGroup.permanent) {
this.formGroup.disable();
} else {
this.formGroup.enable();
}
}
})
);
}
/**
* Stop editing the currently selected group
*/
onCancel() {
this.groupDataService.cancelEditGroup();
this.cancelForm.emit();
this.routeService.getPreviousUrl().pipe(
take(1),
).subscribe((previousURL) => {
void this.router.navigate([previousURL && previousURL.trim().length > 0 ? previousURL : getGroupsRoute()]);
});
}
/**
* Submit the form
* When the eperson has an id attached -> Edit the eperson
* When the eperson has no id attached -> Create new eperson
* Emit the updated/created eperson using the EventEmitter submitForm
*/
onSubmit() {
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
if (group === null) {
this.createNewGroup({
name: this.groupName.value,
metadata: {
'dc.description': [
{
value: this.groupDescription.value,
},
],
},
});
} else {
this.editGroup(group);
}
});
}
/**
* Creates new Group based on given values from form
* @param values
*/
createNewGroup(values) {
const groupToCreate = Object.assign(new Group(), values);
this.groupDataService.create(groupToCreate).pipe(
getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name }));
this.submitForm.emit(groupToCreate);
if (isNotEmpty(rd.payload)) {
const groupSelfLink = rd.payload._links.self.href;
this.setActiveGroupWithLink(groupSelfLink);
this.groupDataService.clearGroupsRequests();
void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid));
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
this.showNotificationIfNameInUse(groupToCreate, 'created');
this.cancelForm.emit();
}
});
}
/**
* Checks for the given group if there is already a group in the system with that group name and shows error if that
* is the case
* @param group group to check
* @param notificationSection whether in create or edit
*/
private showNotificationIfNameInUse(group: Group, notificationSection: string) {
// Relevant message for group name in use
this.subs.push(this.groupDataService.searchGroups(group.name, {
currentPage: 1,
elementsPerPage: 0
}).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload())
.subscribe((list: PaginatedList<Group>) => {
if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', {
name: this.dsoNameService.getName(group),
}));
}
}));
}
/**
* Edit existing Group based on given values from form and old Group
* @param group Group to edit and old values contained within
*/
editGroup(group: Group) {
let operations: Operation[] = [];
if (hasValue(this.groupDescription.value)) {
operations = [...operations, {
op: 'add',
path: '/metadata/dc.description',
value: this.groupDescription.value
}];
}
if (hasValue(this.groupName.value)) {
operations = [...operations, {
op: 'replace',
path: '/name',
value: this.groupName.value
}];
}
this.groupDataService.patch(group, operations).pipe(
getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) }));
this.submitForm.emit(rd.payload);
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: this.dsoNameService.getName(group) }));
this.cancelForm.emit();
}
});
}
/**
* Start editing the selected group
* @param groupId ID of group to set as active
*/
setActiveGroup(groupId: string) {
this.groupDataService.cancelEditGroup();
this.groupDataService.findById(groupId)
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload())
.subscribe((group: Group) => {
this.groupDataService.editGroup(group);
});
}
/**
* Start editing the selected group
* @param groupSelfLink SelfLink of group to set as active
*/
setActiveGroupWithLink(groupSelfLink: string) {
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup === null) {
this.groupDataService.cancelEditGroup();
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload())
.subscribe((group: Group) => {
this.groupDataService.editGroup(group);
});
}
});
}
/**
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
* It'll either show a success or error message depending on whether the delete was successful or not.
*/
delete() {
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = group;
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-group.modal.info';
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-group.modal.cancel';
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-group.modal.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
if (hasValue(group.id)) {
this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData())
.subscribe((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: this.dsoNameService.getName(group) }));
this.onCancel();
} else {
this.notificationsService.error(
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: this.dsoNameService.getName(group) }),
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage }));
}
});
}
}
});
});
}
/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
@HostListener('window:beforeunload')
ngOnDestroy(): void {
this.groupDataService.cancelEditGroup();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
if ( hasValue(this.groupNameValueChangeSubscribe) ) {
this.groupNameValueChangeSubscribe.unsubscribe();
}
}
/**
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
* workflow group)
*/
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
return this.activeGroup$.pipe(
hasValueOperator(),
switchMap((group: Group) => {
if (group.object === undefined) {
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
}
return group.object;
}),
getAllCompletedRemoteData(),
getRemoteDataPayload(),
);
}
/**
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
* to a workflow group) if it has one
*/
getLinkedEditRolesRoute(): Observable<string> {
return this.activeGroupLinkedDSO$.pipe(
hasValueOperator(),
map((dso: DSpaceObject) => {
switch ((dso as any).type) {
case Community.type.value:
return getCommunityEditRolesRoute(dso.id);
case Collection.type.value:
return getCollectionEditRolesRoute(dso.id);
}
}),
);
}
}