Skip to content

Commit

Permalink
Merge pull request #874 from atmire/w2p-72956_delete-eperson
Browse files Browse the repository at this point in the history
Delete eperson
  • Loading branch information
tdonohue authored Oct 8, 2020
2 parents 5bf6d0f + fbe6ec6 commit d9634a8
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>

<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>

<div *ngIf="!isEPersonFormShown">
Expand Down Expand Up @@ -40,10 +40,10 @@ <h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | trans
</form>

<ds-pagination
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
*ngIf="(ePeopleDto$ | async)?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeople | async)?.payload"
[collectionSize]="(ePeople | async)?.payload?.totalElements"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
Expand All @@ -59,21 +59,21 @@ <h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | trans
</tr>
</thead>
<tbody>
<tr *ngFor="let eperson of (ePeople | async)?.payload?.page"
[ngClass]="{'table-primary' : isActive(eperson) | async}">
<td>{{eperson.id}}</td>
<td>{{eperson.name}}</td>
<td>{{eperson.email}}</td>
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
<td>{{epersonDto.eperson.id}}</td>
<td>{{epersonDto.eperson.name}}</td>
<td>{{epersonDto.eperson.email}}</td>
<td>
<div class="btn-group edit-field">
<button (click)="toggleEditEPerson(eperson)"
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: eperson.name} }}">
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button (click)="deleteEPerson(eperson)"
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: eperson.name} }}">
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
Expand All @@ -85,7 +85,7 @@ <h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | trans

</ds-pagination>

<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { getMockTranslateService } from '../../../shared/mocks/translate.service
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { RequestService } from '../../../core/data/request.service';

describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
Expand All @@ -33,6 +35,8 @@ describe('EPeopleRegistryComponent', () => {

let mockEPeople;
let ePersonDataServiceStub: any;
let authorizationService: AuthorizationDataService;
let modalService;

beforeEach(async(() => {
mockEPeople = [EPersonMock, EPersonMock2];
Expand Down Expand Up @@ -82,6 +86,9 @@ describe('EPeopleRegistryComponent', () => {
return '/admin/access-control/epeople';
}
};
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
TestBed.configureTestingModule({
Expand All @@ -94,11 +101,13 @@ describe('EPeopleRegistryComponent', () => {
}),
],
declarations: [EPeopleRegistryComponent],
providers: [EPeopleRegistryComponent,
providers: [
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterStub() },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
Expand All @@ -107,12 +116,14 @@ describe('EPeopleRegistryComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(EPeopleRegistryComponent);
component = fixture.componentInstance;
modalService = (component as any).modalService;
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
fixture.detectChanges();
});

it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
expect(comp).toBeDefined();
}));
it('should create EPeopleRegistryComponent', () => {
expect(component).toBeDefined();
});

it('should display list of ePeople', () => {
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
Expand Down Expand Up @@ -215,4 +226,20 @@ describe('EPeopleRegistryComponent', () => {
});
});

describe('delete EPerson button when the isAuthorized returns false', () => {
let ePeopleDeleteButton;
beforeEach(() => {
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(false)
});
});

it ('should be disabled', () => {
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
ePeopleDeleteButton.forEach((deleteButton) => {
expect(deleteButton.nativeElement.disabled).toBe(true);
});

})
})
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { Subscription } from 'rxjs/internal/Subscription';
import { map, take } from 'rxjs/operators';
import { map, switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RequestService } from '../../../core/data/request.service';
import { filter } from 'rxjs/internal/operators/filter';
import { PageInfo } from '../../../core/shared/page-info.model';

@Component({
selector: 'ds-epeople-registry',
Expand All @@ -28,7 +38,17 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
/**
* A list of all the current EPeople within the repository or the result of the search
*/
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
ePeople$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject<RemoteData<PaginatedList<EPerson>>>({} as any);
/**
* A BehaviorSubject with the list of EpersonDtoModel objects made from the EPeople in the repository or
* as the result of the search
*/
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);

/**
* An observable for the pageInfo, needed to pass to the pagination component
*/
pageInfoState$: BehaviorSubject<PageInfo> = new BehaviorSubject<PageInfo>(undefined);

/**
* Pagination config used to display the list of epeople
Expand Down Expand Up @@ -59,8 +79,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
constructor(private epersonService: EPersonDataService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private authorizationService: AuthorizationDataService,
private formBuilder: FormBuilder,
private router: Router) {
private router: Router,
private modalService: NgbModal,
public requestService: RequestService) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
this.searchForm = this.formBuilder.group(({
Expand All @@ -70,6 +93,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
}

ngOnInit() {
this.initialisePage();
}

/**
* This method will initialise the page
*/
initialisePage() {
this.isEPersonFormShown = false;
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
Expand All @@ -84,18 +114,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
* @param event
*/
onPageChange(event) {
this.config.currentPage = event;
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
}

/**
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
* a new REST call
*/
public forceUpdateEPeople() {
this.epersonService.clearEPersonRequests();
this.isEPersonFormShown = false;
this.search({ query: '', scope: 'metadata' })
if (this.config.currentPage !== event) {
this.config.currentPage = event;
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
}
}

/**
Expand All @@ -115,10 +137,33 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.currentSearchScope = scope;
this.config.currentPage = 1;
}
this.ePeople = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
this.subs.push(this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize
});
}).subscribe((peopleRD) => {
this.ePeople$.next(peopleRD)
}
));

this.subs.push(this.ePeople$.pipe(
getAllSucceededRemoteDataPayload(),
switchMap((epeople) => {
return combineLatest(...epeople.page.map((eperson) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
map((authorized) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.ableToDelete = authorized;
epersonDtoModel.eperson = eperson;
return epersonDtoModel;
})
);
})).pipe(map((dtos: EpersonDtoModel[]) => {
return new PaginatedList(epeople.pageInfo, dtos);
}))
})).subscribe((value) => {
this.ePeopleDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
}));
}

/**
Expand Down Expand Up @@ -160,23 +205,37 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/
deleteEPerson(ePerson: EPerson) {
if (hasValue(ePerson.id)) {
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => {
if (success) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
this.forceUpdateEPeople();
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name }));
}
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
})
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = 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.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
if (hasValue(ePerson.id)) {
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
this.reset();
} else {
const errorResponse = restResponse as ErrorResponse;
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + errorResponse.statusCode + ' and message: ' + errorResponse.errorMessage);
}
})
}}
});
}
}

/**
* Unsub all subscriptions
*/
ngOnDestroy(): void {
this.cleanupSubscribes();
}

cleanupSubscribes() {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}

Expand All @@ -199,4 +258,18 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
});
this.search({ query: '' });
}

/**
* This method will ensure that the page gets reset and that the cache is cleared
*/
reset() {
this.epersonService.getBrowseEndpoint().pipe(
switchMap((href) => this.requestService.removeByHrefSubstring(href)),
filter((isCached) => isCached),
take(1)
).subscribe(() => {
this.cleanupSubscribes();
this.initialisePage();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ <h4>{{messagePrefix + '.edit' | translate}}</h4>
<button class="btn btn-light" [disabled]="!(canReset$ | async)">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button>
<button class="btn btn-light" [disabled]="!(canDelete$ | async)">
<button class="btn btn-light delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
</button>
<button *ngIf="!isImpersonated" class="btn btn-light" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
Expand Down
Loading

0 comments on commit d9634a8

Please sign in to comment.