mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into angular-cli
This commit is contained in:
@@ -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 {
|
||||||
|
|
||||||
|
}
|
@@ -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 {
|
||||||
|
|
||||||
|
}
|
@@ -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
|
@@ -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>
|
@@ -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 { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||||
|
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
|
||||||
|
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: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
|
||||||
|
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
|
||||||
|
import { EPersonMock } from '../../../shared/testing/eperson.mock';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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 { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||||
|
import { EPersonFormComponent } from './eperson-form.component';
|
||||||
|
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||||
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||||
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
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: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
@@ -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 },
|
||||||
|
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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
|
||||||
],
|
],
|
||||||
|
@@ -35,6 +35,12 @@ export function getAdminModulePath() {
|
|||||||
return `/${ADMIN_MODULE_PATH}`;
|
return `/${ADMIN_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROFILE_MODULE_PATH = 'profile';
|
||||||
|
|
||||||
|
export function getProfileModulePath() {
|
||||||
|
return `/${PROFILE_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDSOPath(dso: DSpaceObject): string {
|
export function getDSOPath(dso: DSpaceObject): string {
|
||||||
switch ((dso as any).type) {
|
switch ((dso as any).type) {
|
||||||
case Community.type.value:
|
case Community.type.value:
|
||||||
@@ -66,6 +72,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||||
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
|
||||||
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
|
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
|
||||||
|
{ path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -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;
|
||||||
|
@@ -23,7 +23,7 @@ import { NativeWindowRef, NativeWindowService } from '../services/window.service
|
|||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
@@ -149,7 +149,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
||||||
return this.epersonService.findByHref(userHref).pipe(
|
return this.epersonService.findByHref(userHref).pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getAllSucceededRemoteDataPayload()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
src/app/core/cache/object-cache.service.ts
vendored
4
src/app/core/cache/object-cache.service.ts
vendored
@@ -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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -50,6 +50,14 @@ describe('ServerSyncBufferEffects', () => {
|
|||||||
_links: { self: { href: link } }
|
_links: { self: { href: link } }
|
||||||
});
|
});
|
||||||
return observableOf(object);
|
return observableOf(object);
|
||||||
|
},
|
||||||
|
getBySelfLink: (link) => {
|
||||||
|
const object = Object.assign(new DSpaceObject(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: link }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return observableOf(object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
18
src/app/core/cache/server-sync-buffer.effects.ts
vendored
18
src/app/core/cache/server-sync-buffer.effects.ts
vendored
@@ -14,7 +14,7 @@ import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/s
|
|||||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { PutRequest } from '../data/request.models';
|
import { PatchRequest, PutRequest } from '../data/request.models';
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
@@ -22,6 +22,8 @@ import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
|||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { ObjectCacheEntry } from './object-cache.reducer';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerSyncBufferEffects {
|
export class ServerSyncBufferEffects {
|
||||||
@@ -95,14 +97,16 @@ export class ServerSyncBufferEffects {
|
|||||||
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||||
*/
|
*/
|
||||||
private applyPatch(href: string): Observable<Action> {
|
private applyPatch(href: string): Observable<Action> {
|
||||||
const patchObject = this.objectCache.getObjectBySelfLink(href).pipe(take(1));
|
const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1));
|
||||||
|
|
||||||
return patchObject.pipe(
|
return patchObject.pipe(
|
||||||
map((object) => {
|
map((entry: ObjectCacheEntry) => {
|
||||||
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
if (isNotEmpty(entry.patches)) {
|
||||||
|
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||||
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
if (isNotEmpty(flatPatch)) {
|
||||||
|
this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch));
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ApplyPatchObjectCacheAction(href);
|
return new ApplyPatchObjectCacheAction(href);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -56,7 +56,7 @@ export abstract class BaseResponseParsingService {
|
|||||||
.filter((property) => data._embedded.hasOwnProperty(property))
|
.filter((property) => data._embedded.hasOwnProperty(property))
|
||||||
.forEach((property) => {
|
.forEach((property) => {
|
||||||
const parsedObj = this.process<ObjectDomain>(data._embedded[property], request);
|
const parsedObj = this.process<ObjectDomain>(data._embedded[property], request);
|
||||||
if (this.shouldDirectlyAttachEmbeds && isNotEmpty(parsedObj)) {
|
if (hasValue(object) && this.shouldDirectlyAttachEmbeds && isNotEmpty(parsedObj)) {
|
||||||
if (isRestPaginatedList(data._embedded[property])) {
|
if (isRestPaginatedList(data._embedded[property])) {
|
||||||
object[property] = parsedObj;
|
object[property] = parsedObj;
|
||||||
object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj));
|
object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj));
|
||||||
|
@@ -16,9 +16,10 @@ import { Item } from '../shared/item.model';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { FindListOptions } from './request.models';
|
import { FindListOptions, PatchRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import * as decorators from "../cache/builders/build-decorators";
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -64,8 +65,8 @@ describe('DataService', () => {
|
|||||||
let store;
|
let store;
|
||||||
|
|
||||||
function initTestService(): TestService {
|
function initTestService(): TestService {
|
||||||
requestService = { generateRequestId: () => uuidv4() } as RequestService;
|
requestService = getMockRequestService();
|
||||||
halService = {} as HALEndpointService;
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
rdbService = {} as RemoteDataBuildService;
|
rdbService = {} as RemoteDataBuildService;
|
||||||
notificationsService = {} as NotificationsService;
|
notificationsService = {} as NotificationsService;
|
||||||
http = {} as HttpClient;
|
http = {} as HttpClient;
|
||||||
@@ -229,18 +230,23 @@ describe('DataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('patch', () => {
|
describe('patch', () => {
|
||||||
let operations;
|
const dso = {
|
||||||
let selfLink;
|
uuid: 'dso-uuid'
|
||||||
|
};
|
||||||
|
const operations = [
|
||||||
|
Object.assign({
|
||||||
|
op: 'move',
|
||||||
|
from: '/1',
|
||||||
|
path: '/5'
|
||||||
|
}) as Operation
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
operations = [{ op: 'replace', path: '/metadata/dc.title', value: 'random string' } as Operation];
|
service.patch(dso, operations);
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
|
||||||
spyOn(objectCache, 'addPatch');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call addPatch on the object cache with the right parameters', () => {
|
it('should configure a PatchRequest', () => {
|
||||||
service.patch(selfLink, operations);
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
||||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ import {
|
|||||||
FindByIDRequest,
|
FindByIDRequest,
|
||||||
FindListOptions,
|
FindListOptions,
|
||||||
FindListRequest,
|
FindListRequest,
|
||||||
GetRequest
|
GetRequest, PatchRequest
|
||||||
} from './request.models';
|
} from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -329,12 +329,28 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new patch to the object cache to a specified object
|
* Send a patch request for a specified object
|
||||||
* @param {string} href The selflink of the object that will be patched
|
* @param {T} dso The object to send a patch request for
|
||||||
* @param {Operation[]} operations The patch operations to be performed
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
*/
|
*/
|
||||||
patch(href: string, operations: Operation[]) {
|
patch(dso: T, operations: Operation[]): Observable<RestResponse> {
|
||||||
this.objectCache.addPatch(href, operations);
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, dso.uuid)));
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PatchRequest(requestId, href, operations);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,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
|
||||||
|
@@ -3,6 +3,8 @@ import { compare } from 'fast-json-patch';
|
|||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { MetadataMap } from '../shared/metadata.models';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to determine what differs between two
|
* A class to determine what differs between two
|
||||||
@@ -21,6 +23,21 @@ export class DSOChangeAnalyzer<T extends DSpaceObject> implements ChangeAnalyzer
|
|||||||
* The second object to compare
|
* The second object to compare
|
||||||
*/
|
*/
|
||||||
diff(object1: DSpaceObject, object2: DSpaceObject): Operation[] {
|
diff(object1: DSpaceObject, object2: DSpaceObject): Operation[] {
|
||||||
return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
return compare(this.filterUUIDsFromMetadata(object1.metadata), this.filterUUIDsFromMetadata(object2.metadata))
|
||||||
|
.map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the UUIDs out of a MetadataMap
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
filterUUIDsFromMetadata(metadata: MetadataMap): MetadataMap {
|
||||||
|
const result = cloneDeep(metadata);
|
||||||
|
for (const key of Object.keys(result)) {
|
||||||
|
for (const metadataValue of result[key]) {
|
||||||
|
metadataValue.uuid = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -119,6 +119,8 @@ export class HeadRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PatchRequest extends RestRequest {
|
export class PatchRequest extends RestRequest {
|
||||||
|
public responseMsToLive = 60 * 15 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
|
301
src/app/core/eperson/eperson-data.service.spec.ts
Normal file
301
src/app/core/eperson/eperson-data.service.spec.ts
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
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 { 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';
|
||||||
|
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
|
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: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<ds-form *ngIf="formModel"
|
||||||
|
[formId]="'profile-page-metadata-form-id'"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[displaySubmit]="false">
|
||||||
|
</ds-form>
|
@@ -0,0 +1,138 @@
|
|||||||
|
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
|
describe('ProfilePageMetadataFormComponent', () => {
|
||||||
|
let component: ProfilePageMetadataFormComponent;
|
||||||
|
let fixture: ComponentFixture<ProfilePageMetadataFormComponent>;
|
||||||
|
|
||||||
|
let user;
|
||||||
|
|
||||||
|
let epersonService;
|
||||||
|
let notificationsService;
|
||||||
|
let translate;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
user = Object.assign(new EPerson(), {
|
||||||
|
email: 'example@gmail.com',
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: 'John',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: 'Doe',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.language': [
|
||||||
|
{
|
||||||
|
value: 'de',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
epersonService = jasmine.createSpyObj('epersonService', {
|
||||||
|
update: createSuccessfulRemoteDataObject$(user)
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
success: {},
|
||||||
|
error: {},
|
||||||
|
warning: {}
|
||||||
|
});
|
||||||
|
translate = {
|
||||||
|
instant: () => 'translated',
|
||||||
|
onLangChange: new EventEmitter()
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProfilePageMetadataFormComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: EPersonDataService, useValue: epersonService },
|
||||||
|
{ provide: TranslateService, useValue: translate },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
FormBuilderService
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProfilePageMetadataFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.user = user;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill in the user\'s email in the correct field', () => {
|
||||||
|
expect(component.formGroup.get('email').value).toEqual(user.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically fill the present metadata values and leave missing ones empty', () => {
|
||||||
|
expect(component.formGroup.get('firstname').value).toEqual('John');
|
||||||
|
expect(component.formGroup.get('lastname').value).toEqual('Doe');
|
||||||
|
expect(component.formGroup.get('phone').value).toBeUndefined();
|
||||||
|
expect(component.formGroup.get('language').value).toEqual('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateProfile', () => {
|
||||||
|
describe('when no values changed', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call epersonService.update', () => {
|
||||||
|
expect(epersonService.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a form value changed', () => {
|
||||||
|
let result;
|
||||||
|
let newUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
newUser = cloneDeep(user);
|
||||||
|
newUser.metadata['eperson.firstname'][0].value = 'Johnny';
|
||||||
|
setModelValue('firstname', 'Johnny');
|
||||||
|
result = component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call epersonService.update', () => {
|
||||||
|
expect(epersonService.update).toHaveBeenCalledWith(newUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setModelValue(id: string, value: string) {
|
||||||
|
component.formModel.filter((model) => model.id === id).forEach((model) => (model as any).value = value);
|
||||||
|
}
|
||||||
|
});
|
@@ -0,0 +1,211 @@
|
|||||||
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormValueControlModel,
|
||||||
|
DynamicInputModel, DynamicSelectModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { LangConfig } from '../../../config/lang-config.interface';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-profile-page-metadata-form',
|
||||||
|
templateUrl: './profile-page-metadata-form.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for a user to edit their metadata
|
||||||
|
* Displays a form containing:
|
||||||
|
* - readonly email field,
|
||||||
|
* - required first name text field
|
||||||
|
* - required last name text field
|
||||||
|
* - phone text field
|
||||||
|
* - language dropdown
|
||||||
|
*/
|
||||||
|
export class ProfilePageMetadataFormComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The user to display the form for
|
||||||
|
*/
|
||||||
|
@Input() user: EPerson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form's input models
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'email',
|
||||||
|
name: 'email',
|
||||||
|
readOnly: true
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'firstname',
|
||||||
|
name: 'eperson.firstname',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'This field is required'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'lastname',
|
||||||
|
name: 'eperson.lastname',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'This field is required'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'phone',
|
||||||
|
name: 'eperson.phone'
|
||||||
|
}),
|
||||||
|
new DynamicSelectModel<string>({
|
||||||
|
id: 'language',
|
||||||
|
name: 'eperson.language'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form group of this form
|
||||||
|
*/
|
||||||
|
formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the form's label messages of this component
|
||||||
|
*/
|
||||||
|
LABEL_PREFIX = 'profile.metadata.form.label.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the form's error messages of this component
|
||||||
|
*/
|
||||||
|
ERROR_PREFIX = 'profile.metadata.form.error.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the notification messages of this component
|
||||||
|
*/
|
||||||
|
NOTIFICATION_PREFIX = 'profile.metadata.form.notifications.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All of the configured active languages
|
||||||
|
* Used to populate the language dropdown
|
||||||
|
*/
|
||||||
|
activeLangs: LangConfig[];
|
||||||
|
|
||||||
|
constructor(protected formBuilderService: FormBuilderService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected epersonService: EPersonDataService,
|
||||||
|
protected notificationsService: NotificationsService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activeLangs = environment.languages.filter((MyLangConfig) => MyLangConfig.active === true);
|
||||||
|
this.setFormValues();
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
this.translate.onLangChange
|
||||||
|
.subscribe(() => {
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop over all the form's input models and set their values depending on the user's metadata
|
||||||
|
* Create the FormGroup
|
||||||
|
*/
|
||||||
|
setFormValues() {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicInputModel | DynamicSelectModel<string>) => {
|
||||||
|
if (fieldModel.name === 'email') {
|
||||||
|
fieldModel.value = this.user.email;
|
||||||
|
} else {
|
||||||
|
fieldModel.value = this.user.firstMetadataValue(fieldModel.name);
|
||||||
|
}
|
||||||
|
if (fieldModel.id === 'language') {
|
||||||
|
(fieldModel as DynamicSelectModel<string>).options =
|
||||||
|
this.activeLangs.map((langConfig) => Object.assign({ value: langConfig.code, label: langConfig.label }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the translations of the field labels and error messages
|
||||||
|
*/
|
||||||
|
updateFieldTranslations() {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicInputModel) => {
|
||||||
|
fieldModel.label = this.translate.instant(this.LABEL_PREFIX + fieldModel.id);
|
||||||
|
if (isNotEmpty(fieldModel.validators)) {
|
||||||
|
fieldModel.errorMessages = {};
|
||||||
|
Object.keys(fieldModel.validators).forEach((key) => {
|
||||||
|
fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_PREFIX + fieldModel.id + '.' + key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's metadata
|
||||||
|
*
|
||||||
|
* Sends a patch request for updating the user's metadata when at least one value changed or got added/removed and the
|
||||||
|
* form is valid.
|
||||||
|
* Nothing happens when the form is invalid or no metadata changed.
|
||||||
|
*
|
||||||
|
* Returns false when nothing happened.
|
||||||
|
*/
|
||||||
|
updateProfile(): boolean {
|
||||||
|
if (!this.formGroup.valid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMetadata = cloneDeep(this.user.metadata);
|
||||||
|
let changed = false;
|
||||||
|
this.formModel.filter((fieldModel) => fieldModel.id !== 'email').forEach((fieldModel: DynamicFormValueControlModel<string>) => {
|
||||||
|
if (newMetadata.hasOwnProperty(fieldModel.name) && newMetadata[fieldModel.name].length > 0) {
|
||||||
|
if (hasValue(fieldModel.value)) {
|
||||||
|
if (newMetadata[fieldModel.name][0].value !== fieldModel.value) {
|
||||||
|
newMetadata[fieldModel.name][0].value = fieldModel.value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMetadata[fieldModel.name] = [];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if (hasValue(fieldModel.value)) {
|
||||||
|
newMetadata[fieldModel.name] = [{
|
||||||
|
value: fieldModel.value,
|
||||||
|
language: null
|
||||||
|
} as any];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.epersonService.update(Object.assign(cloneDeep(this.user), {metadata: newMetadata})).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload()
|
||||||
|
).subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
this.setFormValues();
|
||||||
|
this.notificationsService.success(
|
||||||
|
this.translate.instant(this.NOTIFICATION_PREFIX + 'success.title'),
|
||||||
|
this.translate.instant(this.NOTIFICATION_PREFIX + 'success.content')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
15
src/app/profile-page/profile-page-routing.module.ts
Normal file
15
src/app/profile-page/profile-page-routing.module.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { ProfilePageComponent } from './profile-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{ path: '', pathMatch: 'full', component: ProfilePageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'profile', title: 'profile.title' } }
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProfilePageRoutingModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="container-fluid mb-4">{{'profile.security.form.info' | translate}}</div>
|
||||||
|
<ds-form *ngIf="formModel"
|
||||||
|
[formId]="'profile-page-security-form-id'"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[displaySubmit]="false">
|
||||||
|
</ds-form>
|
||||||
|
<div class="container-fluid text-danger" *ngIf="formGroup.hasError('notLongEnough')">{{'profile.security.form.error.password-length' | translate}}</div>
|
||||||
|
<div class="container-fluid text-danger" *ngIf="formGroup.hasError('notSame')">{{'profile.security.form.error.matching-passwords' | translate}}</div>
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
|
||||||
|
import { ProfilePageSecurityFormComponent } from './profile-page-security-form.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
|
||||||
|
describe('ProfilePageSecurityFormComponent', () => {
|
||||||
|
let component: ProfilePageSecurityFormComponent;
|
||||||
|
let fixture: ComponentFixture<ProfilePageSecurityFormComponent>;
|
||||||
|
|
||||||
|
let user;
|
||||||
|
|
||||||
|
let epersonService;
|
||||||
|
let notificationsService;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
user = Object.assign(new EPerson(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'user-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
epersonService = jasmine.createSpyObj('epersonService', {
|
||||||
|
patch: observableOf(new RestResponse(true, 200, 'OK'))
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
success: {},
|
||||||
|
error: {},
|
||||||
|
warning: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProfilePageSecurityFormComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: EPersonDataService, useValue: epersonService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
FormBuilderService
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProfilePageSecurityFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.user = user;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSecurity', () => {
|
||||||
|
describe('when no values changed', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = component.updateSecurity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call epersonService.patch', () => {
|
||||||
|
expect(epersonService.patch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when password is filled in, but the confirm field is empty', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setModelValue('password', 'test');
|
||||||
|
result = component.updateSecurity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when both password fields are filled in, long enough and equal', () => {
|
||||||
|
let result;
|
||||||
|
let operations;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setModelValue('password', 'testest');
|
||||||
|
setModelValue('passwordrepeat', 'testest');
|
||||||
|
operations = [{ op: 'replace', path: '/password', value: 'testest' }];
|
||||||
|
result = component.updateSecurity();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return call epersonService.patch', () => {
|
||||||
|
expect(epersonService.patch).toHaveBeenCalledWith(user, operations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setModelValue(id: string, value: string) {
|
||||||
|
component.formGroup.patchValue({
|
||||||
|
[id]: value
|
||||||
|
});
|
||||||
|
component.formGroup.markAllAsTouched();
|
||||||
|
}
|
||||||
|
});
|
@@ -0,0 +1,151 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicInputModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { ErrorResponse, RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-profile-page-security-form',
|
||||||
|
templateUrl: './profile-page-security-form.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for a user to edit their security information
|
||||||
|
* Displays a form containing a password field and a confirmation of the password
|
||||||
|
*/
|
||||||
|
export class ProfilePageSecurityFormComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The user to display the form for
|
||||||
|
*/
|
||||||
|
@Input() user: EPerson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form's input models
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'password',
|
||||||
|
name: 'password',
|
||||||
|
inputType: 'password'
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'passwordrepeat',
|
||||||
|
name: 'passwordrepeat',
|
||||||
|
inputType: 'password'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form group of this form
|
||||||
|
*/
|
||||||
|
formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the notification messages of this component
|
||||||
|
*/
|
||||||
|
NOTIFICATIONS_PREFIX = 'profile.security.form.notifications.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the form's label messages of this component
|
||||||
|
*/
|
||||||
|
LABEL_PREFIX = 'profile.security.form.label.';
|
||||||
|
|
||||||
|
constructor(protected formService: DynamicFormService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected epersonService: EPersonDataService,
|
||||||
|
protected notificationsService: NotificationsService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.formGroup = this.formService.createFormGroup(this.formModel, { validators: [this.checkPasswordsEqual, this.checkPasswordLength] });
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
this.translate.onLangChange
|
||||||
|
.subscribe(() => {
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the translations of the field labels
|
||||||
|
*/
|
||||||
|
updateFieldTranslations() {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicInputModel) => {
|
||||||
|
fieldModel.label = this.translate.instant(this.LABEL_PREFIX + fieldModel.id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if both password fields are filled in and equal
|
||||||
|
* @param group The FormGroup to validate
|
||||||
|
*/
|
||||||
|
checkPasswordsEqual(group: FormGroup) {
|
||||||
|
const pass = group.get('password').value;
|
||||||
|
const repeatPass = group.get('passwordrepeat').value;
|
||||||
|
|
||||||
|
return pass === repeatPass ? null : { notSame: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the password is at least 6 characters long
|
||||||
|
* @param group The FormGroup to validate
|
||||||
|
*/
|
||||||
|
checkPasswordLength(group: FormGroup) {
|
||||||
|
const pass = group.get('password').value;
|
||||||
|
|
||||||
|
return isEmpty(pass) || pass.length >= 6 ? null : { notLongEnough: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's security details
|
||||||
|
*
|
||||||
|
* Sends a patch request for changing the user's password when a new password is present and the password confirmation
|
||||||
|
* matches the new password.
|
||||||
|
* Nothing happens when no passwords are filled in.
|
||||||
|
* An error notification is displayed when the password confirmation does not match the new password.
|
||||||
|
*
|
||||||
|
* Returns false when nothing happened
|
||||||
|
*/
|
||||||
|
updateSecurity() {
|
||||||
|
const pass = this.formGroup.get('password').value;
|
||||||
|
const passEntered = isNotEmpty(pass);
|
||||||
|
if (!this.formGroup.valid) {
|
||||||
|
if (passEntered) {
|
||||||
|
if (this.checkPasswordsEqual(this.formGroup) != null) {
|
||||||
|
this.notificationsService.error(this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.not-same'));
|
||||||
|
}
|
||||||
|
if (this.checkPasswordLength(this.formGroup) != null) {
|
||||||
|
this.notificationsService.error(this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.not-long-enough'));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (passEntered) {
|
||||||
|
const operation = Object.assign({ op: 'replace', path: '/password', value: pass });
|
||||||
|
this.epersonService.patch(this.user, [operation]).subscribe((response: RestResponse) => {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationsService.success(
|
||||||
|
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'success.title'),
|
||||||
|
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'success.content')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.title'), (response as ErrorResponse).errorMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return passEntered;
|
||||||
|
}
|
||||||
|
}
|
27
src/app/profile-page/profile-page.component.html
Normal file
27
src/app/profile-page/profile-page.component.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<ng-container *ngVar="(user$ | async) as user">
|
||||||
|
<div class="container" *ngIf="user">
|
||||||
|
<h3 class="mb-4">{{'profile.head' | translate}}</h3>
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">{{'profile.card.identify' | translate}}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-profile-page-metadata-form [user]="user"></ds-profile-page-metadata-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">{{'profile.card.security' | translate}}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-profile-page-security-form [user]="user"></ds-profile-page-security-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary" (click)="updateProfile()">{{'profile.form.submit' | translate}}</button>
|
||||||
|
|
||||||
|
<ng-container *ngVar="(groupsRD$ | async)?.payload?.page as groups">
|
||||||
|
<div *ngIf="groups">
|
||||||
|
<h3 class="mt-4">{{'profile.groups.head' | translate}}</h3>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li *ngFor="let group of groups" class="list-group-item">{{group.name}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
138
src/app/profile-page/profile-page.component.spec.ts
Normal file
138
src/app/profile-page/profile-page.component.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { ProfilePageComponent } from './profile-page.component';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { AppState } from '../app.reducer';
|
||||||
|
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { authReducer } from '../core/auth/auth.reducer';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../shared/testing/utils.test';
|
||||||
|
|
||||||
|
describe('ProfilePageComponent', () => {
|
||||||
|
let component: ProfilePageComponent;
|
||||||
|
let fixture: ComponentFixture<ProfilePageComponent>;
|
||||||
|
let user;
|
||||||
|
let authState;
|
||||||
|
|
||||||
|
let epersonService;
|
||||||
|
let notificationsService;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
user = Object.assign(new EPerson(), {
|
||||||
|
groups: createSuccessfulRemoteDataObject$(createPaginatedList([]))
|
||||||
|
});
|
||||||
|
authState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
|
user: user
|
||||||
|
};
|
||||||
|
|
||||||
|
epersonService = jasmine.createSpyObj('epersonService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(user)
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
success: {},
|
||||||
|
error: {},
|
||||||
|
warning: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProfilePageComponent, VarDirective],
|
||||||
|
imports: [StoreModule.forRoot(authReducer), TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: EPersonDataService, useValue: epersonService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([Store], (store: Store<AppState>) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = authState;
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ProfilePageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('updateProfile', () => {
|
||||||
|
describe('when the metadata form returns false and the security form returns true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.metadataForm = jasmine.createSpyObj('metadataForm', {
|
||||||
|
updateProfile: false
|
||||||
|
});
|
||||||
|
component.securityForm = jasmine.createSpyObj('securityForm', {
|
||||||
|
updateSecurity: true
|
||||||
|
});
|
||||||
|
component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a warning', () => {
|
||||||
|
expect(notificationsService.warning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata form returns true and the security form returns false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.metadataForm = jasmine.createSpyObj('metadataForm', {
|
||||||
|
updateProfile: true
|
||||||
|
});
|
||||||
|
component.securityForm = jasmine.createSpyObj('securityForm', {
|
||||||
|
updateSecurity: false
|
||||||
|
});
|
||||||
|
component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a warning', () => {
|
||||||
|
expect(notificationsService.warning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata form returns true and the security form returns true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.metadataForm = jasmine.createSpyObj('metadataForm', {
|
||||||
|
updateProfile: true
|
||||||
|
});
|
||||||
|
component.securityForm = jasmine.createSpyObj('securityForm', {
|
||||||
|
updateSecurity: true
|
||||||
|
});
|
||||||
|
component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a warning', () => {
|
||||||
|
expect(notificationsService.warning).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata form returns false and the security form returns false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.metadataForm = jasmine.createSpyObj('metadataForm', {
|
||||||
|
updateProfile: false
|
||||||
|
});
|
||||||
|
component.securityForm = jasmine.createSpyObj('securityForm', {
|
||||||
|
updateSecurity: false
|
||||||
|
});
|
||||||
|
component.updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a warning', () => {
|
||||||
|
expect(notificationsService.warning).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
84
src/app/profile-page/profile-page.component.ts
Normal file
84
src/app/profile-page/profile-page.component.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { getAuthenticatedUser } from '../core/auth/selectors';
|
||||||
|
import { AppState } from '../app.reducer';
|
||||||
|
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component';
|
||||||
|
import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Group } from '../core/eperson/models/group.model';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
|
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-profile-page',
|
||||||
|
templateUrl: './profile-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for a user to edit their profile information
|
||||||
|
*/
|
||||||
|
export class ProfilePageComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* A reference to the metadata form component
|
||||||
|
*/
|
||||||
|
@ViewChild(ProfilePageMetadataFormComponent, { static: false }) metadataForm: ProfilePageMetadataFormComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the security form component
|
||||||
|
*/
|
||||||
|
@ViewChild(ProfilePageSecurityFormComponent, { static: false }) securityForm: ProfilePageSecurityFormComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authenticated user
|
||||||
|
*/
|
||||||
|
user$: Observable<EPerson>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The groups the user belongs to
|
||||||
|
*/
|
||||||
|
groupsRD$: Observable<RemoteData<PaginatedList<Group>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the notification messages of this component
|
||||||
|
*/
|
||||||
|
NOTIFICATIONS_PREFIX = 'profile.notifications.';
|
||||||
|
|
||||||
|
constructor(private store: Store<AppState>,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private epersonService: EPersonDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.user$ = this.store.pipe(
|
||||||
|
select(getAuthenticatedUser),
|
||||||
|
filter((user: EPerson) => hasValue(user.id)),
|
||||||
|
switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))),
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload()
|
||||||
|
);
|
||||||
|
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire an update on both the metadata and security forms
|
||||||
|
* Show a warning notification when no changes were made in both forms
|
||||||
|
*/
|
||||||
|
updateProfile() {
|
||||||
|
const metadataChanged = this.metadataForm.updateProfile();
|
||||||
|
const securityChanged = this.securityForm.updateSecurity();
|
||||||
|
if (!metadataChanged && !securityChanged) {
|
||||||
|
this.notificationsService.warning(
|
||||||
|
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'warning.no-changes.title'),
|
||||||
|
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'warning.no-changes.content')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/app/profile-page/profile-page.module.ts
Normal file
23
src/app/profile-page/profile-page.module.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ProfilePageRoutingModule } from './profile-page-routing.module';
|
||||||
|
import { ProfilePageComponent } from './profile-page.component';
|
||||||
|
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component';
|
||||||
|
import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
ProfilePageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ProfilePageComponent,
|
||||||
|
ProfilePageMetadataFormComponent,
|
||||||
|
ProfilePageSecurityFormComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProfilePageModule {
|
||||||
|
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
<ds-loading *ngIf="(loading$ | async)"></ds-loading>
|
<ds-loading *ngIf="(loading$ | async)"></ds-loading>
|
||||||
<div *ngIf="!(loading$ | async)">
|
<div *ngIf="!(loading$ | async)">
|
||||||
<span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span>
|
<span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span>
|
||||||
|
<a class="dropdown-item" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
|
||||||
<a class="dropdown-item" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
<a class="dropdown-item" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<ds-log-out></ds-log-out>
|
<ds-log-out></ds-log-out>
|
||||||
|
@@ -7,6 +7,7 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
|
|||||||
import { AppState } from '../../../app.reducer';
|
import { AppState } from '../../../app.reducer';
|
||||||
import { getAuthenticatedUser, isAuthenticationLoading } from '../../../core/auth/selectors';
|
import { getAuthenticatedUser, isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||||
import { MYDSPACE_ROUTE } from '../../../+my-dspace-page/my-dspace-page.component';
|
import { MYDSPACE_ROUTE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { getProfileModulePath } from '../../../app-routing.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the user nav menu.
|
* This component represents the user nav menu.
|
||||||
@@ -36,6 +37,11 @@ export class UserMenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public mydspaceRoute = MYDSPACE_ROUTE;
|
public mydspaceRoute = MYDSPACE_ROUTE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The profile page route
|
||||||
|
*/
|
||||||
|
public profileRoute = getProfileModulePath();
|
||||||
|
|
||||||
constructor(private store: Store<AppState>) {
|
constructor(private store: Store<AppState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
16
src/app/shared/testing/group-mock.ts
Normal file
16
src/app/shared/testing/group-mock.ts
Normal 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',
|
||||||
|
});
|
@@ -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",
|
||||||
@@ -1377,6 +1441,8 @@
|
|||||||
|
|
||||||
"nav.mydspace": "MyDSpace",
|
"nav.mydspace": "MyDSpace",
|
||||||
|
|
||||||
|
"nav.profile": "Profile",
|
||||||
|
|
||||||
"nav.search": "Search",
|
"nav.search": "Search",
|
||||||
|
|
||||||
"nav.statistics.header": "Statistics",
|
"nav.statistics.header": "Statistics",
|
||||||
@@ -1435,6 +1501,64 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"profile.breadcrumbs": "Update Profile",
|
||||||
|
|
||||||
|
"profile.card.identify": "Identify",
|
||||||
|
|
||||||
|
"profile.card.security": "Security",
|
||||||
|
|
||||||
|
"profile.form.submit": "Update Profile",
|
||||||
|
|
||||||
|
"profile.groups.head": "Authorization groups you belong to",
|
||||||
|
|
||||||
|
"profile.head": "Update Profile",
|
||||||
|
|
||||||
|
"profile.metadata.form.error.firstname.required": "First Name is required",
|
||||||
|
|
||||||
|
"profile.metadata.form.error.lastname.required": "Last Name is required",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.email": "Email Address",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.firstname": "First Name",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.language": "Language",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.lastname": "Last Name",
|
||||||
|
|
||||||
|
"profile.metadata.form.label.phone": "Contact Telephone",
|
||||||
|
|
||||||
|
"profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.",
|
||||||
|
|
||||||
|
"profile.metadata.form.notifications.success.title": "Profile saved",
|
||||||
|
|
||||||
|
"profile.notifications.warning.no-changes.content": "No changes were made to the Profile.",
|
||||||
|
|
||||||
|
"profile.notifications.warning.no-changes.title": "No changes",
|
||||||
|
|
||||||
|
"profile.security.form.error.matching-passwords": "The passwords do not match.",
|
||||||
|
|
||||||
|
"profile.security.form.error.password-length": "The password should be at least 6 characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.label.password": "Password",
|
||||||
|
|
||||||
|
"profile.security.form.label.passwordrepeat": "Retype to confirm",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.success.content": "Your changes to the password were saved.",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.success.title": "Password saved",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.title": "Error changing passwords",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.not-long-enough": "The password has to be at least 6 characters long.",
|
||||||
|
|
||||||
|
"profile.security.form.notifications.error.not-same": "The provided passwords are not the same.",
|
||||||
|
|
||||||
|
"profile.title": "Update Profile",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"project.listelement.badge": "Research Project",
|
"project.listelement.badge": "Research Project",
|
||||||
|
|
||||||
"project.page.contributor": "Contributors",
|
"project.page.contributor": "Contributors",
|
||||||
|
Reference in New Issue
Block a user