Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,24 @@ <h2 id="search" class="border-bottom pb-2">
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i>
</button>
@if (epersonDto.ableToDelete) {
<button (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
@if (epersonDto.ableToDelete && currentAuthenticatedUserId) {
@if (isCurrentUser(epersonDto.eperson)) {
<span tabindex="0" [ngbTooltip]="selfDeleteWarningLabel | translate" container="body">
<button [dsBtnDisabled]="true"
tabindex="-1"
[attr.aria-label]="selfDeleteWarningLabel | translate"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
type="button">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</span>
} @else {
<button (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
}
}
</div>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
of,
} from 'rxjs';

import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FindListOptions } from '../../core/data/find-list-options.model';
import {
Expand Down Expand Up @@ -57,6 +58,7 @@ import {
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonDeleteGuardService } from './eperson-delete-guard.service';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';

describe('EPeopleRegistryComponent', () => {
Expand All @@ -67,6 +69,8 @@ describe('EPeopleRegistryComponent', () => {
let mockEPeople: EPerson[];
let ePersonDataServiceStub: any;
let authorizationService: AuthorizationDataService;
let authService: jasmine.SpyObj<AuthService>;
let deleteGuard: jasmine.SpyObj<EPersonDeleteGuardService>;
let modalService: NgbModal;
let paginationService: PaginationServiceStub;

Expand Down Expand Up @@ -149,6 +153,12 @@ describe('EPeopleRegistryComponent', () => {
});
builderService = getMockFormBuilderService();

authService = jasmine.createSpyObj('authService', ['getAuthenticatedUserFromStore']);
authService.getAuthenticatedUserFromStore.and.returnValue(of(Object.assign(new EPerson(), { id: 'different-user-id' })));
deleteGuard = jasmine.createSpyObj('deleteGuard', ['isCurrentUser', 'getDeleteWarningLabel', 'isSelfDeletionError', 'showSelfDeleteNotification']);
deleteGuard.isCurrentUser.and.callFake((ePerson: EPerson, currentId: string) => !!ePerson?.id && ePerson.id === currentId);
deleteGuard.getDeleteWarningLabel.and.returnValue(of(undefined));
deleteGuard.isSelfDeletionError.and.returnValue(false);
paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]),
Expand All @@ -157,6 +167,8 @@ describe('EPeopleRegistryComponent', () => {
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: AuthService, useValue: authService },
{ provide: EPersonDeleteGuardService, useValue: deleteGuard },
{ provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterMock() },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
Expand Down Expand Up @@ -257,6 +269,25 @@ describe('EPeopleRegistryComponent', () => {
});
});
});

describe('when the ePerson is the currently authenticated user', () => {
beforeEach(() => {
component.currentAuthenticatedUserId = EPersonMock.id;
fixture.detectChanges();
});

it('renders the delete button for that row as disabled', () => {
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
const disabled = deleteButtons.filter((button) => button.nativeElement.getAttribute('aria-disabled') === 'true');
expect(disabled.length).toBe(1);
});

it('notifies instead of opening the confirmation modal', () => {
component.deleteEPerson(EPersonMock);
expect(deleteGuard.showSelfDeleteNotification).toHaveBeenCalled();
expect(modalService.open).not.toHaveBeenCalled();
});
});
});


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
Router,
RouterModule,
} from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
NgbModal,
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
Expand All @@ -32,6 +35,7 @@ import {
take,
} from 'rxjs/operators';

import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
Expand All @@ -51,6 +55,7 @@ import {
getFirstCompletedRemoteData,
} from '../../core/shared/operators';
import { PageInfo } from '../../core/shared/page-info.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue } from '../../shared/empty.util';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
Expand All @@ -61,14 +66,20 @@ import {
getEPersonEditRoute,
getEPersonsRoute,
} from '../access-control-routing-paths';
import {
EPersonDeleteGuardService,
SELF_DELETE_WARNING_LABEL,
} from './eperson-delete-guard.service';
import { EPersonFormComponent } from './eperson-form/eperson-form.component';

@Component({
selector: 'ds-epeople-registry',
templateUrl: './epeople-registry.component.html',
imports: [
AsyncPipe,
BtnDisabledDirective,
EPersonFormComponent,
NgbTooltipModule,
NgClass,
PaginationComponent,
ReactiveFormsModule,
Expand All @@ -85,6 +96,9 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component';
export class EPeopleRegistryComponent implements OnInit, OnDestroy {

labelPrefix = 'admin.access-control.epeople.';
selfDeleteWarningLabel = SELF_DELETE_WARNING_LABEL;

currentAuthenticatedUserId: string;

/**
* A list of all the current EPeople within the repository or the result of the search
Expand Down Expand Up @@ -138,6 +152,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
private notificationsService: NotificationsService,
private authorizationService: AuthorizationDataService,
private authService: AuthService,
private deleteGuard: EPersonDeleteGuardService,
private formBuilder: UntypedFormBuilder,
private router: Router,
private modalService: NgbModal,
Expand All @@ -164,6 +180,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.searching$.next(true);
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
this.activeEPerson$ = this.epersonService.getActiveEPerson();
this.subs.push(this.authService.getAuthenticatedUserFromStore().subscribe((currentUser: EPerson) => {
this.currentAuthenticatedUserId = currentUser?.id;
}));
this.subs.push(this.ePeople$.pipe(
switchMap((epeople: PaginatedList<EPerson>) => {
if (epeople.pageInfo.totalElements > 0) {
Expand Down Expand Up @@ -236,30 +255,52 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/
deleteEPerson(ePerson: EPerson) {
if (hasValue(ePerson.id)) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.name = this.dsoNameService.getName(ePerson);
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
if (hasValue(ePerson.id)) {
if (!hasValue(this.currentAuthenticatedUserId)) {
return;
}

if (this.isCurrentUser(ePerson)) {
this.deleteGuard.showSelfDeleteNotification();
return;
}

this.deleteGuard.getDeleteWarningLabel(ePerson).pipe(take(1)).subscribe((warningLabel: string | undefined) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.name = this.dsoNameService.getName(ePerson);
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.warningLabel = warningLabel;
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(ePerson) }));
} else if (this.isCurrentUser(ePerson) || this.deleteGuard.isSelfDeletionError(restResponse)) {
this.deleteGuard.showSelfDeleteNotification();
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', {
name: this.dsoNameService.getName(ePerson),
id: ePerson.id,
statusCode: restResponse.statusCode,
errorMessage: restResponse.errorMessage,
restResponse,
}));
}
});
}
}
});
});
}
}

isCurrentUser(ePerson: EPerson): boolean {
return this.deleteGuard.isCurrentUser(ePerson, this.currentAuthenticatedUserId);
}

/**
* Unsub all subscriptions
*/
Expand Down
Loading
Loading