Merge pull request #609 from atmire/manage-epersons-page

Access-control - EPeople admin page
This commit is contained in:
Tim Donohue
2020-03-18 16:16:48 -05:00
committed by GitHub
22 changed files with 1936 additions and 44 deletions

View File

@@ -170,6 +170,70 @@
"admin.access-control.epeople.title": "DSpace Angular :: EPeople",
"admin.access-control.epeople.head": "EPeople",
"admin.access-control.epeople.search.head": "Search",
"admin.access-control.epeople.search.scope.name": "Name",
"admin.access-control.epeople.search.scope.email": "E-mail (exact)",
"admin.access-control.epeople.search.scope.metadata": "Metadata",
"admin.access-control.epeople.search.button": "Search",
"admin.access-control.epeople.button.add": "Add EPerson",
"admin.access-control.epeople.table.id": "ID",
"admin.access-control.epeople.table.name": "Name",
"admin.access-control.epeople.table.email": "E-mail",
"admin.access-control.epeople.table.edit": "Edit",
"item.access-control.epeople.table.edit.buttons.edit": "Edit",
"item.access-control.epeople.table.edit.buttons.remove": "Remove",
"admin.access-control.epeople.no-items": "No EPeople to show.",
"admin.access-control.epeople.form.create": "Create EPerson",
"admin.access-control.epeople.form.edit": "Edit EPerson",
"admin.access-control.epeople.form.firstName": "First name",
"admin.access-control.epeople.form.lastName": "Last name",
"admin.access-control.epeople.form.email": "E-mail",
"admin.access-control.epeople.form.emailHint": "Must be valid e-mail address",
"admin.access-control.epeople.form.canLogIn": "Can log in",
"admin.access-control.epeople.form.requireCertificate": "Requires certificate",
"admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"",
"admin.search.breadcrumbs": "Administrative Search", "admin.search.breadcrumbs": "Administrative Search",
"admin.search.collection.edit": "Edit", "admin.search.collection.edit": "Edit",
@@ -1560,7 +1624,7 @@
"relationships.isSingleVolumeOf": "Journal Volume", "relationships.isSingleVolumeOf": "Journal Volume",
"relationships.isVolumeOf": "Journal Volumes", "relationships.isVolumeOf": "Journal Volumes",
"relationships.isContributorOf": "Contributors", "relationships.isContributorOf": "Contributors",

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
])
]
})
/**
* Routing module for the AccessControl section of the admin sidebar
*/
export class AdminAccessControlRoutingModule {
}

View File

@@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../shared/shared.module';
import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
RouterModule,
TranslateModule,
AdminAccessControlRoutingModule
],
declarations: [
EPeopleRegistryComponent,
EPersonFormComponent
],
entryComponents: []
})
/**
* This module handles all components related to the access control pages
*/
export class AdminAccessControlModule {
}

View File

@@ -0,0 +1,49 @@
import { Action } from '@ngrx/store';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { type } from '../../../shared/ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const EPeopleRegistryActionTypes = {
EDIT_EPERSON: type('dspace/epeople-registry/EDIT_EPERSON'),
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
};
/* tslint:disable:max-classes-per-file */
/**
* Used to edit an EPerson in the EPeople registry
*/
export class EPeopleRegistryEditEPersonAction implements Action {
type = EPeopleRegistryActionTypes.EDIT_EPERSON;
eperson: EPerson;
constructor(eperson: EPerson) {
this.eperson = eperson;
}
}
/**
* Used to cancel the editing of an EPerson in the EPeople registry
*/
export class EPeopleRegistryCancelEPersonAction implements Action {
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
}
/* tslint:enable:max-classes-per-file */
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
* These are all the actions to perform on the EPeople registry state
*/
export type EPeopleRegistryAction
= EPeopleRegistryEditEPersonAction
| EPeopleRegistryCancelEPersonAction

View File

@@ -0,0 +1,90 @@
<div class="container">
<div class="epeople-registry row">
<div class="col-12">
<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
<div *ngIf="!isEPersonFormShown" class="button-row top d-flex pb-2">
<button class="mr-auto btn btn-success addEPerson-button"
(click)="isEPersonFormShown = true">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
</button>
</div>
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12 col-sm-3">
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select>
</div>
<div class="col-sm-9 col-12">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
<span class="input-group-append">
<button type="submit"
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
</span>
</div>
</div>
</form>
<ds-pagination
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeople | async)?.payload"
[collectionSize]="(ePeople | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<div class="table-responsive">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
<th>{{labelPrefix + 'table.edit' | translate}}</th>
</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>
<td>
<div class="btn-group edit-field">
<button (click)="toggleEditEPerson(eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button (click)="deleteEPerson(eperson)"
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,205 @@
import { of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RemoteData } from '../../../core/data/remote-data';
import { FindListOptions } from '../../../core/data/request.models';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { EPeopleRegistryComponent } from './epeople-registry.component';
describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
let fixture: ComponentFixture<EPeopleRegistryComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService;
const mockEPeople = [EPersonMock, EPersonMock2];
let ePersonDataServiceStub: any;
beforeEach(async(() => {
ePersonDataServiceStub = {
activeEPerson: null,
allEpeople: mockEPeople,
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> {
return observableOf(this.activeEPerson);
},
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
if (scope === 'email') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return ePerson.email === query
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
if (scope === 'metadata') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query))
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
deleteEPerson(ePerson: EPerson): Observable<boolean> {
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
return (ePerson2.uuid !== ePerson.uuid);
});
return observableOf(true);
},
editEPerson(ePerson: EPerson) {
this.activeEPerson = ePerson;
},
cancelEditEPerson() {
this.activeEPerson = null;
},
clearEPersonRequests(): void {
// empty
}
};
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [EPeopleRegistryComponent],
providers: [EPeopleRegistryComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService },
EPeopleRegistryComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EPeopleRegistryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
expect(comp).toBeDefined();
}));
it('should display list of ePeople', () => {
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
expect(ePeopleIdsFound.length).toEqual(2);
mockEPeople.map((ePerson: EPerson) => {
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
})).toBeTruthy();
})
});
describe('search', () => {
describe('when searching with scope/query (scope metadata)', () => {
let ePeopleIdsFound;
beforeEach(fakeAsync(() => {
component.search({ scope: 'metadata', query: EPersonMock2.name });
tick();
fixture.detectChanges();
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('should display search result', () => {
expect(ePeopleIdsFound.length).toEqual(1);
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
})).toBeTruthy();
});
});
describe('when searching with scope/query (scope email)', () => {
let ePeopleIdsFound;
beforeEach(fakeAsync(() => {
component.search({ scope: 'email', query: EPersonMock.email });
tick();
fixture.detectChanges();
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('should display search result', () => {
expect(ePeopleIdsFound.length).toEqual(1);
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
})).toBeTruthy();
});
});
});
describe('toggleEditEPerson', () => {
describe('when you click on first edit eperson button', () => {
beforeEach(fakeAsync(() => {
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
editButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
}));
it('editEPerson form is toggled', () => {
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
expect(component.isEPersonFormShown).toEqual(false);
} else {
expect(component.isEPersonFormShown).toEqual(true);
}
})
});
});
});
describe('deleteEPerson', () => {
describe('when you click on first delete eperson button', () => {
let ePeopleIdsFoundBeforeDelete;
let ePeopleIdsFoundAfterDelete;
beforeEach(fakeAsync(() => {
ePeopleIdsFoundBeforeDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
ePeopleIdsFoundAfterDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('first ePerson is deleted', () => {
expect(ePeopleIdsFoundBeforeDelete.length === ePeopleIdsFoundAfterDelete + 1);
ePeopleIdsFoundAfterDelete.forEach((epersonElement) => {
expect(epersonElement !== ePeopleIdsFoundBeforeDelete[0].nativeElement.textContent).toBeTrue();
});
});
});
});
});

View File

@@ -0,0 +1,163 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map, 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';
@Component({
selector: 'ds-epeople-registry',
templateUrl: './epeople-registry.component.html',
})
/**
* A component used for managing all existing epeople within the repository.
* The admin can create, edit or delete epeople here.
*/
export class EPeopleRegistryComponent {
labelPrefix = 'admin.access-control.epeople.';
/**
* A list of all the current EPeople within the repository or the result of the search
*/
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
/**
* Pagination config used to display the list of epeople
*/
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'epeople-list-pagination',
pageSize: 5,
currentPage: 1
});
/**
* Whether or not to show the EPerson form
*/
isEPersonFormShown: boolean;
// The search form
searchForm;
constructor(private epersonService: EPersonDataService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private formBuilder: FormBuilder) {
this.updateEPeople({
currentPage: 1,
elementsPerPage: this.config.pageSize
});
this.isEPersonFormShown = false;
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
}
/**
* Event triggered when the user changes page
* @param event
*/
onPageChange(event) {
this.updateEPeople({
currentPage: event,
elementsPerPage: this.config.pageSize
});
}
/**
* Update the list of EPeople by fetching it from the rest api or cache
*/
private updateEPeople(options) {
this.ePeople = this.epersonService.getEPeople(options);
}
/**
* 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' })
}
/**
* Search in the EPeople by metadata (default) or email
* @param data Contains scope and query param
*/
search(data: any) {
this.ePeople = this.epersonService.searchByScope(data.scope, data.query, {
currentPage: 1,
elementsPerPage: this.config.pageSize
});
}
/**
* Checks whether the given EPerson is active (being edited)
* @param eperson
*/
isActive(eperson: EPerson): Observable<boolean> {
return this.getActiveEPerson().pipe(
map((activeEPerson) => eperson === activeEPerson)
);
}
/**
* Gets the active eperson (being edited)
*/
getActiveEPerson(): Observable<EPerson> {
return this.epersonService.getActiveEPerson();
}
/**
* Start editing the selected EPerson
* @param ePerson
*/
toggleEditEPerson(ePerson: EPerson) {
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
if (ePerson === activeEPerson) {
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
} else {
this.epersonService.editEPerson(ePerson);
this.isEPersonFormShown = true;
}
});
this.scrollToTop()
}
/**
* Deletes EPerson, show notification on success/failure & updates EPeople list
*/
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;
})
}
}
scrollToTop() {
(function smoothscroll() {
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
if (currentScroll > 0) {
window.requestAnimationFrame(smoothscroll);
window.scrollTo(0, currentScroll - (currentScroll / 8));
}
})();
}
}

View File

@@ -0,0 +1,54 @@
import { EPersonMock } from '../../../shared/testing/eperson-mock';
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
const initialState: EPeopleRegistryState = {
editEPerson: null,
};
const editState: EPeopleRegistryState = {
editEPerson: EPersonMock,
};
class NullAction extends EPeopleRegistryEditEPersonAction {
type = null;
constructor() {
super(undefined);
}
}
describe('epeopleRegistryReducer', () => {
it('should return the current state when no valid actions have been made', () => {
const state = initialState;
const action = new NullAction();
const newState = ePeopleRegistryReducer(state, action);
expect(newState).toEqual(state);
});
it('should start with an initial state', () => {
const state = initialState;
const action = new NullAction();
const initState = ePeopleRegistryReducer(undefined, action);
expect(initState).toEqual(state);
});
it('should update the current state to change the editEPerson to a new eperson when EPeopleRegistryEditEPersonAction is dispatched', () => {
const state = editState;
const action = new EPeopleRegistryEditEPersonAction(EPersonMock);
const newState = ePeopleRegistryReducer(state, action);
expect(newState.editEPerson).toEqual(EPersonMock);
});
it('should update the current state to remove the editEPerson from the state when EPeopleRegistryCancelEPersonAction is dispatched', () => {
const state = editState;
const action = new EPeopleRegistryCancelEPersonAction();
const newState = ePeopleRegistryReducer(state, action);
expect(newState.editEPerson).toEqual(null);
});
});

View File

@@ -0,0 +1,47 @@
import { EPerson } from '../../../core/eperson/models/eperson.model';
import {
EPeopleRegistryAction,
EPeopleRegistryActionTypes,
EPeopleRegistryEditEPersonAction
} from './epeople-registry.actions';
/**
* The EPeople registry state.
* @interface EPeopleRegistryState
*/
export interface EPeopleRegistryState {
editEPerson: EPerson;
}
/**
* The initial state.
*/
const initialState: EPeopleRegistryState = {
editEPerson: null,
};
/**
* Reducer that handles EPeopleRegistryActions to modify EPeople
* @param state The current EPeopleRegistryState
* @param action The EPeopleRegistryAction to perform on the state
*/
export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegistryAction): EPeopleRegistryState {
switch (action.type) {
case EPeopleRegistryActionTypes.EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson
});
}
case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: null
});
}
default:
return state;
}
}

View File

@@ -0,0 +1,17 @@
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
<ng-template #createHeader>
<h4>{{messagePrefix + '.create' | translate}}</h4>
</ng-template>
<ng-template #editheader>
<h4>{{messagePrefix + '.edit' | translate}}</h4>
</ng-template>
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
(cancel)="onCancel()"
(submitForm)="onSubmit()">
</ds-form>

View File

@@ -0,0 +1,206 @@
import { of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../../../../core/cache/response.models';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { FindListOptions } from '../../../../core/data/request.models';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { EPeopleRegistryComponent } from '../epeople-registry.component';
import { EPersonFormComponent } from './eperson-form.component';
describe('EPersonFormComponent', () => {
let component: EPersonFormComponent;
let fixture: ComponentFixture<EPersonFormComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService;
const mockEPeople = [EPersonMock, EPersonMock2];
let ePersonDataServiceStub: any;
beforeEach(async(() => {
ePersonDataServiceStub = {
activeEPerson: null,
allEpeople: mockEPeople,
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> {
return observableOf(this.activeEPerson);
},
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
if (scope === 'email') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return ePerson.email === query
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
if (scope === 'metadata') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query))
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
deleteEPerson(ePerson: EPerson): Observable<boolean> {
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
return (ePerson2.uuid !== ePerson.uuid);
});
return observableOf(true);
},
create(ePerson: EPerson) {
this.allEpeople = [...this.allEpeople, ePerson]
},
editEPerson(ePerson: EPerson) {
this.activeEPerson = ePerson;
},
cancelEditEPerson() {
this.activeEPerson = null;
},
clearEPersonRequests(): void {
// empty
},
tryToCreate(ePerson: EPerson): Observable<RestResponse> {
this.allEpeople = [...this.allEpeople, ePerson]
return observableOf(new RestResponse(true, 200, 'Success'));
},
updateEPerson(ePerson: EPerson): Observable<RestResponse> {
this.allEpeople.forEach((ePersonInList: EPerson, i: number) => {
if (ePersonInList.id === ePerson.id) {
this.allEpeople[i] = ePerson;
}
});
return observableOf(new RestResponse(true, 200, 'Success'));
}
};
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
providers: [EPersonFormComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService },
EPeopleRegistryComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EPersonFormComponent);
component = fixture.componentInstance;
component.ngOnInit();
fixture.detectChanges();
});
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
expect(comp).toBeDefined();
}));
describe('when submitting the form', () => {
const firstName = 'testName';
const lastName = 'testLastName';
const email = 'testEmail@test.com';
const canLogIn = false;
const requireCertificate = false;
const expected = Object.assign(new EPerson(), {
metadata: {
'eperson.firstname': [
{
value: firstName
}
],
'eperson.lastname': [
{
value: lastName
},
],
},
email: email,
canLogIn: canLogIn,
requireCertificate: requireCertificate,
});
beforeEach(() => {
spyOn(component.submitForm, 'emit');
component.firstName.value = firstName;
component.lastName.value = lastName;
component.email.value = email;
component.canLogIn.value = canLogIn;
component.requireCertificate.value = requireCertificate;
});
describe('without active EPerson', () => {
beforeEach(() => {
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
component.onSubmit();
fixture.detectChanges();
});
it('should emit a new eperson using the correct values', async(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
});
}));
});
describe('with an active eperson', () => {
const expectedWithId = Object.assign(new EPerson(), {
metadata: {
'eperson.firstname': [
{
value: firstName
}
],
'eperson.lastname': [
{
value: lastName
},
],
},
email: email,
canLogIn: canLogIn,
requireCertificate: requireCertificate,
});
beforeEach(() => {
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
component.onSubmit();
fixture.detectChanges();
});
it('should emit the existing eperson using the correct values', async(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
});
}));
});
});
});

View File

@@ -0,0 +1,344 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicCheckboxModel,
DynamicFormControlModel,
DynamicFormLayout,
DynamicInputModel
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
import { Subscription } from 'rxjs/internal/Subscription';
import { take } from 'rxjs/operators';
import { RestResponse } from '../../../../core/cache/response.models';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
@Component({
selector: 'ds-eperson-form',
templateUrl: './eperson-form.component.html'
})
/**
* A form used for creating and editing EPeople
*/
export class EPersonFormComponent implements OnInit, OnDestroy {
labelPrefix = 'admin.access-control.epeople.form.';
/**
* A unique id used for ds-form
*/
formId = 'eperson-form';
/**
* The labelPrefix for all messages related to this form
*/
messagePrefix = 'admin.access-control.epeople.form';
/**
* Dynamic input models for the inputs of form
*/
firstName: DynamicInputModel;
lastName: DynamicInputModel;
email: DynamicInputModel;
// booleans
canLogIn: DynamicCheckboxModel;
requireCertificate: DynamicCheckboxModel;
/**
* A list of all dynamic input models
*/
formModel: DynamicFormControlModel[];
/**
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
firstName: {
grid: {
host: 'row'
}
},
lastName: {
grid: {
host: 'row'
}
},
email: {
grid: {
host: 'row'
}
},
canLogIn: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
requireCertificate: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
};
/**
* A FormGroup that combines all inputs
*/
formGroup: FormGroup;
/**
* 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[] = [];
/**
* Try to retrieve initial active eperson, to fill in checkboxes at component creation
*/
epersonInitial: EPerson;
constructor(public epersonService: EPersonDataService,
private formBuilderService: FormBuilderService,
private translateService: TranslateService,
private notificationsService: NotificationsService,) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.epersonInitial = eperson;
}));
}
ngOnInit() {
combineLatest(
this.translateService.get(`${this.messagePrefix}.firstName`),
this.translateService.get(`${this.messagePrefix}.lastName`),
this.translateService.get(`${this.messagePrefix}.email`),
this.translateService.get(`${this.messagePrefix}.canLogIn`),
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
this.translateService.get(`${this.messagePrefix}.emailHint`),
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
this.firstName = new DynamicInputModel({
id: 'firstName',
label: firstName,
name: 'firstName',
validators: {
required: null,
},
required: true,
});
this.lastName = new DynamicInputModel({
id: 'lastName',
label: lastName,
name: 'lastName',
validators: {
required: null,
},
required: true,
});
this.email = new DynamicInputModel({
id: 'email',
label: email,
name: 'email',
validators: {
required: null,
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
},
required: true,
hint: emailHint
});
this.canLogIn = new DynamicCheckboxModel(
{
id: 'canLogIn',
label: canLogIn,
name: 'canLogIn',
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
});
this.requireCertificate = new DynamicCheckboxModel(
{
id: 'requireCertificate',
label: requireCertificate,
name: 'requireCertificate',
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
});
this.formModel = [
this.firstName,
this.lastName,
this.email,
this.canLogIn,
this.requireCertificate,
];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.formGroup.patchValue({
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
email: eperson != null ? eperson.email : '',
canLogIn: eperson != null ? eperson.canLogIn : true,
requireCertificate: eperson != null ? eperson.requireCertificate : false
});
}));
});
}
/**
* Stop editing the currently selected eperson
*/
onCancel() {
this.epersonService.cancelEditEPerson();
this.cancelForm.emit();
}
/**
* 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.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
(ePerson: EPerson) => {
console.log('onsubmit ep', ePerson)
const values = {
metadata: {
'eperson.firstname': [
{
value: this.firstName.value
}
],
'eperson.lastname': [
{
value: this.lastName.value
},
],
},
email: this.email.value,
canLogIn: this.canLogIn.value,
requireCertificate: this.requireCertificate.value,
};
if (ePerson == null) {
this.createNewEPerson(values);
} else {
this.editEPerson(ePerson, values);
}
}
);
}
/**
* Creates new EPerson based on given values from form
* @param values
*/
createNewEPerson(values) {
console.log('createNewEPerson(values)', values)
const ePersonToCreate = Object.assign(new EPerson(), values);
const response = this.epersonService.tryToCreate(ePersonToCreate);
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
this.submitForm.emit(ePersonToCreate);
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
this.cancelForm.emit();
}
});
this.showNotificationIfEmailInUse(ePersonToCreate, 'created');
}
/**
* Edits existing EPerson based on given values from form and old EPerson
* @param ePerson ePerson to edit
* @param values new ePerson values (of form)
*/
editEPerson(ePerson: EPerson, values) {
const editedEperson = Object.assign(new EPerson(), {
id: ePerson.id,
metadata: {
'eperson.firstname': [
{
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname'))
}
],
'eperson.lastname': [
{
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname'))
},
],
},
email: (hasValue(values.email) ? values.email : ePerson.email),
canLogIn: (hasValue(values.canLogIn) ? values.canLogIn : ePerson.canLogIn),
requireCertificate: (hasValue(values.requireCertificate) ? values.requireCertificate : ePerson.requireCertificate),
_links: ePerson._links,
});
const response = this.epersonService.updateEPerson(editedEperson);
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
this.submitForm.emit(editedEperson);
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
this.cancelForm.emit();
}
});
if (values.email != null && values.email !== ePerson.email) {
this.showNotificationIfEmailInUse(editedEperson, 'edited');
}
}
/**
* Checks for the given ePerson if there is already an ePerson in the system with that email
* and shows notification if this is the case
* @param ePerson ePerson values to check
* @param notificationSection whether in create or edit
*/
private showNotificationIfEmailInUse(ePerson: EPerson, notificationSection: string) {
// Relevant message for email in use
this.subs.push(this.epersonService.searchByScope('email', ePerson.email, {
currentPage: 1,
elementsPerPage: 0
}).pipe(getSucceededRemoteData(), getRemoteDataPayload())
.subscribe((list: PaginatedList<EPerson>) => {
if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
name: ePerson.name,
email: ePerson.email
}));
}
}));
}
/**
* Reset all input-fields to be empty
*/
clearFields() {
this.formGroup.patchValue({
firstName: '',
lastName: '',
email: '',
canLogin: true,
requireCertificate: false
});
}
/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
ngOnDestroy(): void {
this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -6,6 +6,7 @@ import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
const REGISTRIES_MODULE_PATH = 'registries'; const REGISTRIES_MODULE_PATH = 'registries';
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
export function getRegistriesModulePath() { export function getRegistriesModulePath() {
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString(); return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
@@ -18,6 +19,10 @@ export function getRegistriesModulePath() {
path: REGISTRIES_MODULE_PATH, path: REGISTRIES_MODULE_PATH,
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule' loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
}, },
{
path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: './admin-access-control/admin-access-control.module#AdminAccessControlModule'
},
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },

View File

@@ -1,23 +1,23 @@
import { Component, Injector, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { MenuComponent } from '../../shared/menu/menu.component';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { AuthService } from '../../core/auth/auth.service';
import { first, map } from 'rxjs/operators';
import { combineLatest as combineLatestObservable } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model'; import { combineLatest as combineLatestObservable } from 'rxjs';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; import { Observable } from 'rxjs/internal/Observable';
import { first, map } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import {CreateItemParentSelectorComponent} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
import { MenuComponent } from '../../shared/menu/menu.component';
import { MenuService } from '../../shared/menu/menu.service';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -325,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.access_control_people', text: 'menu.section.access_control_people',
link: '' link: '/admin/access-control/epeople'
} as LinkMenuItemModel, } as LinkMenuItemModel,
}, },
{ {

View File

@@ -1,7 +1,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module'; import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module'; import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '../shared/shared.module';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
import { SearchPageModule } from '../+search-page/search-page.module'; import { SearchPageModule } from '../+search-page/search-page.module';
import { ItemAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component'; import { ItemAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
@@ -14,8 +15,9 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin
@NgModule({ @NgModule({
imports: [ imports: [
AdminRegistriesModule,
AdminRoutingModule, AdminRoutingModule,
AdminRegistriesModule,
AdminAccessControlModule,
SharedModule, SharedModule,
SearchPageModule SearchPageModule
], ],

View File

@@ -1,30 +1,34 @@
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer'; import {
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; ePeopleRegistryReducer,
import { formReducer, FormState } from './shared/form/form.reducer'; EPeopleRegistryState
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; } from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { import {
metadataRegistryReducer, metadataRegistryReducer,
MetadataRegistryState MetadataRegistryState
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers'; } from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { hasValue } from './shared/empty.util'; import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import {
NameVariantListsState,
nameVariantReducer
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { import {
selectableListReducer, selectableListReducer,
SelectableListsState SelectableListsState
} from './shared/object-list/selectable-list/selectable-list.reducer'; } from './shared/object-list/selectable-list/selectable-list.reducer';
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer'; import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
import { import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
NameVariantListsState,
nameVariantReducer import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
@@ -42,6 +46,7 @@ export interface AppState {
selectableLists: SelectableListsState; selectableLists: SelectableListsState;
relationshipLists: NameVariantListsState; relationshipLists: NameVariantListsState;
communityList: CommunityListState; communityList: CommunityListState;
epeopleRegistry: EPeopleRegistryState;
} }
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
@@ -60,6 +65,7 @@ export const appReducers: ActionReducerMap<AppState> = {
selectableLists: selectableListReducer, selectableLists: selectableListReducer,
relationshipLists: nameVariantReducer, relationshipLists: nameVariantReducer,
communityList: CommunityListReducer, communityList: CommunityListReducer,
epeopleRegistry: ePeopleRegistryReducer,
}; };
export const routerStateSelector = (state: AppState) => state.router; export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -10,6 +10,7 @@ import { coreSelector } from '../core.selectors';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
import { selfLinkFromUuidSelector } from '../index/index.selectors'; import { selfLinkFromUuidSelector } from '../index/index.selectors';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { getClassForType } from './builders/build-decorators';
import { LinkService } from './builders/link.service'; import { LinkService } from './builders/link.service';
import { import {
AddPatchObjectCacheAction, AddPatchObjectCacheAction,
@@ -20,7 +21,6 @@ import {
import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
import { AddToSSBAction } from './server-sync-buffer.actions'; import { AddToSSBAction } from './server-sync-buffer.actions';
import { getClassForType } from './builders/build-decorators';
/** /**
* The base selector function to select the object cache in the store * The base selector function to select the object cache in the store
@@ -48,7 +48,7 @@ export class ObjectCacheService {
constructor( constructor(
private store: Store<CoreState>, private store: Store<CoreState>,
private linkService: LinkService private linkService: LinkService
) { ) {
} }
/** /**

View File

@@ -306,7 +306,7 @@ export abstract class DataService<T extends CacheableObject> {
* @return {Observable<RemoteData<PaginatedList<T>>} * @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server * Return an observable that emits response from the server
*/ */
protected searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> { searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
@@ -425,6 +425,48 @@ export abstract class DataService<T extends CacheableObject> {
) )
} }
/**
* Create a new DSpaceObject on the server, and store the response
* in the object cache, returns observable of the response to determine success
*
* @param {DSpaceObject} dso
* The object to create
*/
tryToCreate(dso: T): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId();
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
isNotEmptyOperator(),
distinctUntilChanged(),
);
const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso);
const request$ = endpoint$.pipe(
take(1),
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
);
// Execute the post request
request$.pipe(
configureRequest(this.requestService)
).subscribe();
return this.fetchResponse(requestId);
}
/**
* Gets the restResponse from the requestService
* @param requestId
*/
protected fetchResponse(requestId: string): Observable<RestResponse> {
return this.requestService.getByUUID(requestId).pipe(
getResponseFromEntry(),
map((response: RestResponse) => {
return response;
})
);
}
/** /**
* Delete an existing DSpace Object on the server * Delete an existing DSpace Object on the server
* @param dsoID The DSpace Object' id to be removed * @param dsoID The DSpace Object' id to be removed

View File

@@ -0,0 +1,304 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { compare, Operation } from 'fast-json-patch';
import { getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { TestScheduler } from 'rxjs/testing';
import {
EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
import { SearchParam } from '../cache/models/search-param.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { ChangeAnalyzer } from '../data/change-analyzer';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { DeleteByIDRequest, FindListOptions, PatchRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
import { PageInfo } from '../shared/page-info.model';
import { EPersonDataService } from './eperson-data.service';
import { EPerson } from './models/eperson.model';
describe('EPersonDataService', () => {
let service: EPersonDataService;
let store: Store<CoreState>;
let requestService: RequestService;
let scheduler: TestScheduler;
const epeople = [EPersonMock, EPersonMock2];
const restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
const epersonsEndpoint = `${restEndpointURL}/epersons`;
let halService: any = new HALEndpointServiceStub(restEndpointURL);
const epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople]));
const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ });
const objectCache = Object.assign({
/* tslint:disable:no-empty */
remove: () => {
},
hasBySelfLinkObservable: () => observableOf(false)
/* tslint:enable:no-empty */
}) as ObjectCacheService;
TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.forRoot({}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
const getRequestEntry$ = (successful: boolean) => {
return observableOf({
completed: true,
response: { isSuccessful: successful, payload: epeople } as any
} as RequestEntry)
};
function initTestService() {
return new EPersonDataService(
requestService,
rdbService,
store,
null,
halService,
null,
null,
new DummyChangeAnalyzer() as any
);
}
beforeEach(() => {
requestService = getMockRequestService(getRequestEntry$(true));
store = new Store<CoreState>(undefined, undefined, undefined);
service = initTestService();
spyOn(store, 'dispatch');
});
describe('searchByScope', () => {
beforeEach(() => {
spyOn(service, 'searchBy');
});
it('search by default scope (byMetadata) and no query', () => {
service.searchByScope(null, '');
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', ''))]
});
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
});
it('search metadata scope and no query', () => {
service.searchByScope('metadata', '');
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', ''))]
});
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
});
it('search metadata scope and with query', () => {
service.searchByScope('metadata', 'test');
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', 'test'))]
});
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
});
it('search email scope and no query', () => {
service.searchByScope('email', '');
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('email', ''))]
});
expect(service.searchBy).toHaveBeenCalledWith('byEmail', options);
});
});
describe('updateEPerson', () => {
beforeEach(() => {
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
});
describe('change Email', () => {
const newEmail = 'changedemail@test.com';
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: EPersonMock.metadata,
email: newEmail,
canLogIn: EPersonMock.canLogIn,
requireCertificate: EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with replace email operation', () => {
const operations = [{ op: 'replace', path: '/email', value: newEmail }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
describe('change certificate', () => {
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: EPersonMock.metadata,
email: EPersonMock.email,
canLogIn: EPersonMock.canLogIn,
requireCertificate: !EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with replace certificate operation', () => {
const operations = [{ op: 'replace', path: '/certificate', value: !EPersonMock.requireCertificate }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
describe('change canLogin', () => {
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: EPersonMock.metadata,
email: EPersonMock.email,
canLogIn: !EPersonMock.canLogIn,
requireCertificate: EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with replace canLogIn operation', () => {
const operations = [{ op: 'replace', path: '/canLogIn', value: !EPersonMock.canLogIn }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
describe('change name', () => {
const newFirstName = 'changedFirst';
const newLastName = 'changedLast';
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: {
'eperson.firstname': [
{
value: newFirstName,
}
],
'eperson.lastname': [
{
value: newLastName,
},
],
},
email: EPersonMock.email,
canLogIn: EPersonMock.canLogIn,
requireCertificate: EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with replace name metadata operations', () => {
const operations = [
{ op: 'replace', path: '/eperson.lastname/0/value', value: newLastName },
{ op: 'replace', path: '/eperson.firstname/0/value', value: newFirstName }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
});
describe('clearEPersonRequests', () => {
beforeEach(async(() => {
scheduler = getTestScheduler();
halService = {
getEndpoint(linkPath: string): Observable<string> {
return observableOf(restEndpointURL + '/' + linkPath);
}
} as HALEndpointService;
initTestService();
service.clearEPersonRequests();
}));
it('should remove the eperson hrefs in the request service', () => {
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(epersonsEndpoint);
});
});
describe('getActiveEPerson', () => {
it('should retrieve the ePerson currently getting edited, if any', () => {
service.editEPerson(EPersonMock);
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
expect(activeEPerson).toEqual(EPersonMock);
})
});
it('should retrieve the ePerson currently getting edited, null if none being edited', () => {
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
expect(activeEPerson).toEqual(null);
})
})
});
describe('cancelEditEPerson', () => {
it('should dispatch a CANCEL_EDIT_EPERSON action', () => {
service.cancelEditEPerson();
expect(store.dispatch).toHaveBeenCalledWith(new EPeopleRegistryCancelEPersonAction());
});
});
describe('editEPerson', () => {
it('should dispatch a EDIT_EPERSON action with the EPerson to start editing', () => {
service.editEPerson(EPersonMock);
expect(store.dispatch).toHaveBeenCalledWith(new EPeopleRegistryEditEPersonAction(EPersonMock));
});
});
describe('deleteEPerson', () => {
beforeEach(() => {
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(EPersonMock));
service.deleteEPerson(EPersonMock).subscribe();
});
it('should send DeleteRequest', () => {
const expected = new DeleteByIDRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, EPersonMock.uuid);
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
});
});
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
return observableOf(new RemoteData(false, false, true, undefined, obj));
}
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
diff(object1: Item, object2: Item): Operation[] {
return compare((object1 as any).metadata, (object2 as any).metadata);
}
}

View File

@@ -1,31 +1,52 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch/lib/core';
import { Observable } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import {
EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
import { AppState } from '../../app.reducer';
import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers'; import { RestResponse } from '../cache/response.models';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { FindListOptions, FindListRequest, PatchRequest, } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
import { EPERSON } from './models/eperson.resource-type'; import { EPERSON } from './models/eperson.resource-type';
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
/** /**
* A service to retrieve {@link EPerson}s from the REST API * A service to retrieve {@link EPerson}s from the REST API & EPerson related CRUD actions
*/ */
@Injectable() @Injectable()
@dataService(EPERSON) @dataService(EPERSON)
export class EPersonDataService extends DataService<EPerson> { export class EPersonDataService extends DataService<EPerson> {
protected linkPath = 'epersons'; protected linkPath = 'epersons';
protected searchByEmailPath = 'byEmail';
protected searchByMetadataPath = 'byMetadata';
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<any>,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
@@ -35,4 +56,167 @@ export class EPersonDataService extends DataService<EPerson> {
super(); super();
} }
/**
* Retrieves all EPeople
* @param options The options info used to retrieve the EPeople
*/
public getEPeople(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
const hrefObs = this.getFindAllHref(options, this.linkPath);
hrefObs.pipe(
filter((href: string) => hasValue(href)),
take(1))
.subscribe((href: string) => {
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
});
return this.rdbService.buildList<EPerson>(hrefObs) as Observable<RemoteData<PaginatedList<EPerson>>>;
}
/**
* Search the EPeople with a given scope and query
* @param scope Scope of the EPeople search, default byMetadata
* @param query Query of search
* @param options Options of search request
*/
public searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
switch (scope) {
case 'metadata':
return this.getEpeopleByMetadata(query.trim(), options);
case 'email':
return this.getEpeopleByEmail(query.trim(), options);
default:
return this.getEpeopleByMetadata(query.trim(), options);
}
}
/**
* Returns a search result list of EPeople, by email query (/eperson/epersons/search/{@link searchByEmailPath}?email=<>)
* @param query email query
* @param options
* @param linksToFollow
*/
private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new SearchParam('email', query)];
return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow);
}
/**
* Returns a search result list of EPeople, by metadata query (/eperson/epersons/search/{@link searchByMetadataPath}?query=<>)
* @param query metadata query
* @param options
* @param linksToFollow
*/
private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new SearchParam('query', query)];
return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow);
}
/**
* Returns a search result list of EPeople in a given searchMethod, with given searchParams
* @param searchParams query parameters in the search
* @param searchMethod searchBy path
* @param options
* @param linksToFollow
*/
private getEPeopleBy(searchParams: SearchParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
let findListOptions = new FindListOptions();
if (options) {
findListOptions = Object.assign(new FindListOptions(), options);
}
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
findListOptions.searchParams = searchParams;
}
return this.searchBy(searchMethod, findListOptions, ...linksToFollow);
}
/**
* Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache
* @param {DSpaceObject} ePerson The given object
*/
public updateEPerson(ePerson: EPerson): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId();
const oldVersion$ = this.findByHref(ePerson._links.self.href);
oldVersion$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((oldEPerson: EPerson) => {
const operations = this.generateOperations(oldEPerson, ePerson);
const patchRequest = new PatchRequest(requestId, ePerson._links.self.href, operations);
return this.requestService.configure(patchRequest);
}),
take(1)
).subscribe();
return this.fetchResponse(requestId);
}
/**
* Metadata operations are generated by the difference between old and new EPerson
* Custom replace operations for the other EPerson values
* @param oldEPerson
* @param newEPerson
*/
private generateOperations(oldEPerson: EPerson, newEPerson: EPerson): Operation[] {
let operations = this.comparator.diff(oldEPerson, newEPerson).filter((operation: Operation) => operation.op === 'replace');
if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) {
operations = [...operations, {
op: 'replace', path: '/email', value: newEPerson.email
}]
}
if (hasValue(oldEPerson.requireCertificate) && oldEPerson.requireCertificate !== newEPerson.requireCertificate) {
operations = [...operations, {
op: 'replace', path: '/certificate', value: newEPerson.requireCertificate
}]
}
if (hasValue(oldEPerson.canLogIn) && oldEPerson.canLogIn !== newEPerson.canLogIn) {
operations = [...operations, {
op: 'replace', path: '/canLogIn', value: newEPerson.canLogIn
}]
}
return operations;
}
/**
* Method that clears a cached EPerson request and returns its REST url
*/
public clearEPersonRequests(): void {
this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => {
this.requestService.removeByHrefSubstring(link);
});
}
/**
* Method to retrieve the eperson that is currently being edited
*/
public getActiveEPerson(): Observable<EPerson> {
return this.store.pipe(select(editEPersonSelector))
}
/**
* Method to cancel editing an EPerson, dispatches a cancel EPerson action
*/
public cancelEditEPerson() {
this.store.dispatch(new EPeopleRegistryCancelEPersonAction());
}
/**
* Method to set the EPerson being edited, dispatches an edit EPerson action
* @param ePerson The EPerson to edit
*/
public editEPerson(ePerson: EPerson) {
this.store.dispatch(new EPeopleRegistryEditEPersonAction(ePerson));
}
/**
* Method to delete an EPerson
* @param ePerson The EPerson to delete
*/
public deleteEPerson(ePerson: EPerson): Observable<boolean> {
return this.delete(ePerson.id);
}
} }

View File

@@ -1,6 +1,7 @@
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { GroupMock } from './group-mock';
export const EPersonMock: EPerson = Object.assign(new EPerson(),{ export const EPersonMock: EPerson = Object.assign(new EPerson(), {
handle: null, handle: null,
groups: [], groups: [],
netid: 'test@test.com', netid: 'test@test.com',
@@ -12,7 +13,8 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{
_links: { _links: {
self: { self: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid', href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid',
} },
groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid/groups' }
}, },
id: 'testid', id: 'testid',
uuid: 'testid', uuid: 'testid',
@@ -44,3 +46,49 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{
] ]
} }
}); });
export const EPersonMock2: EPerson = Object.assign(new EPerson(), {
handle: null,
groups: [GroupMock],
netid: 'test2@test.com',
lastActive: '2019-05-14T12:25:42.411+0000',
canLogIn: false,
email: 'test2@test.com',
requireCertificate: false,
selfRegistered: true,
_links: {
self: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid2',
},
groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid2/groups' }
},
id: 'testid2',
uuid: 'testid2',
type: 'eperson',
metadata: {
'dc.title': [
{
language: null,
value: 'User Test 2'
}
],
'eperson.firstname': [
{
language: null,
value: 'User2'
}
],
'eperson.lastname': [
{
language: null,
value: 'MeepMeep'
},
],
'eperson.language': [
{
language: null,
value: 'fr'
},
]
}
});

View File

@@ -0,0 +1,16 @@
import { Group } from '../../core/eperson/models/group.model';
export const GroupMock: Group = Object.assign(new Group(), {
handle: null,
groups: [],
selfRegistered: false,
_links: {
self: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid',
},
groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups' }
},
id: 'testgroupid',
uuid: 'testgroupid',
type: 'group',
});