mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'remotes/origin/master' into shibboleth
# Conflicts: # src/app/app-routing.module.ts # src/app/core/auth/auth.service.ts # src/app/shared/shared.module.ts
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -34,3 +34,5 @@ yarn-error.log
|
||||
*.css
|
||||
|
||||
package-lock.json
|
||||
|
||||
.java-version
|
||||
|
@@ -170,6 +170,98 @@
|
||||
|
||||
|
||||
|
||||
"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.collection.edit": "Edit",
|
||||
|
||||
"admin.search.community.edit": "Edit",
|
||||
|
||||
"admin.search.item.delete": "Delete",
|
||||
|
||||
"admin.search.item.edit": "Edit",
|
||||
|
||||
"admin.search.item.make-private": "Make Private",
|
||||
|
||||
"admin.search.item.make-public": "Make Public",
|
||||
|
||||
"admin.search.item.move": "Move",
|
||||
|
||||
"admin.search.item.private": "Private",
|
||||
|
||||
"admin.search.item.reinstate": "Reinstate",
|
||||
|
||||
"admin.search.item.withdraw": "Withdraw",
|
||||
|
||||
"admin.search.item.withdrawn": "Withdrawn",
|
||||
|
||||
"admin.search.title": "Administrative Search",
|
||||
|
||||
|
||||
|
||||
"auth.errors.invalid-user": "Invalid email address or password.",
|
||||
|
||||
"auth.messages.expired": "Your session has expired. Please log in again.",
|
||||
@@ -913,6 +1005,12 @@
|
||||
|
||||
"item.edit.tabs.status.title": "Item Edit - Status",
|
||||
|
||||
"item.edit.tabs.versionhistory.head": "Version History",
|
||||
|
||||
"item.edit.tabs.versionhistory.title": "Item Edit - Version History",
|
||||
|
||||
"item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.",
|
||||
|
||||
"item.edit.tabs.view.head": "View Item",
|
||||
|
||||
"item.edit.tabs.view.title": "Item Edit - View",
|
||||
@@ -992,6 +1090,25 @@
|
||||
"item.select.table.title": "Title",
|
||||
|
||||
|
||||
"item.version.history.empty": "There are no other versions for this item yet.",
|
||||
|
||||
"item.version.history.head": "Version History",
|
||||
|
||||
"item.version.history.return": "Return",
|
||||
|
||||
"item.version.history.selected": "Selected version",
|
||||
|
||||
"item.version.history.table.version": "Version",
|
||||
|
||||
"item.version.history.table.item": "Item",
|
||||
|
||||
"item.version.history.table.editor": "Editor",
|
||||
|
||||
"item.version.history.table.date": "Date",
|
||||
|
||||
"item.version.history.table.summary": "Summary",
|
||||
|
||||
|
||||
|
||||
"journal.listelement.badge": "Journal",
|
||||
|
||||
@@ -1121,6 +1238,10 @@
|
||||
|
||||
|
||||
|
||||
"menu.section.admin_search": "Admin Search",
|
||||
|
||||
|
||||
|
||||
"menu.section.browse_community": "This Community",
|
||||
|
||||
"menu.section.browse_community_by_author": "By Author",
|
||||
@@ -1171,18 +1292,10 @@
|
||||
|
||||
|
||||
|
||||
"menu.section.find": "Find",
|
||||
|
||||
"menu.section.find_items": "Items",
|
||||
|
||||
"menu.section.find_private_items": "Private Items",
|
||||
|
||||
"menu.section.find_withdrawn_items": "Withdrawn Items",
|
||||
|
||||
|
||||
|
||||
"menu.section.icon.access_control": "Access Control menu section",
|
||||
|
||||
"menu.section.icon.admin_search": "Admin search menu section",
|
||||
|
||||
"menu.section.icon.control_panel": "Control Panel menu section",
|
||||
|
||||
"menu.section.icon.curation_task": "Curation Task menu section",
|
||||
@@ -1357,6 +1470,8 @@
|
||||
|
||||
"nav.mydspace": "MyDSpace",
|
||||
|
||||
"nav.profile": "Profile",
|
||||
|
||||
"nav.search": "Search",
|
||||
|
||||
"nav.statistics.header": "Statistics",
|
||||
@@ -1415,6 +1530,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.page.contributor": "Contributors",
|
||||
@@ -1502,6 +1675,8 @@
|
||||
|
||||
"search.filters.applied.f.dateSubmitted": "Date submitted",
|
||||
|
||||
"search.filters.applied.f.discoverable": "Private",
|
||||
|
||||
"search.filters.applied.f.entityType": "Item Type",
|
||||
|
||||
"search.filters.applied.f.has_content_in_original_bundle": "Has files",
|
||||
@@ -1513,10 +1688,15 @@
|
||||
"search.filters.applied.f.subject": "Subject",
|
||||
|
||||
"search.filters.applied.f.submitter": "Submitter",
|
||||
|
||||
"search.filters.applied.f.jobTitle": "Job Title",
|
||||
|
||||
"search.filters.applied.f.birthDate.max": "End birth date",
|
||||
|
||||
"search.filters.applied.f.birthDate.min": "Start birth date",
|
||||
|
||||
"search.filters.applied.f.withdrawn": "Withdrawn",
|
||||
|
||||
|
||||
|
||||
"search.filters.filter.author.head": "Author",
|
||||
@@ -1553,6 +1733,10 @@
|
||||
|
||||
"search.filters.filter.dateSubmitted.placeholder": "Date submitted",
|
||||
|
||||
"search.filters.filter.discoverable.head": "Private",
|
||||
|
||||
"search.filters.filter.withdrawn.head": "Withdrawn",
|
||||
|
||||
"search.filters.filter.entityType.head": "Item Type",
|
||||
|
||||
"search.filters.filter.entityType.placeholder": "Item Type",
|
||||
@@ -1609,6 +1793,25 @@
|
||||
|
||||
|
||||
|
||||
"search.filters.entityType.JournalIssue": "Journal Issue",
|
||||
|
||||
"search.filters.entityType.JournalVolume": "Journal Volume",
|
||||
|
||||
"search.filters.entityType.OrgUnit": "Organizational Unit",
|
||||
|
||||
"search.filters.has_content_in_original_bundle.true": "Yes",
|
||||
|
||||
"search.filters.has_content_in_original_bundle.false": "No",
|
||||
|
||||
"search.filters.discoverable.true": "No",
|
||||
|
||||
"search.filters.discoverable.false": "Yes",
|
||||
|
||||
"search.filters.withdrawn.true": "Yes",
|
||||
|
||||
"search.filters.withdrawn.false": "No",
|
||||
|
||||
|
||||
"search.filters.head": "Filters",
|
||||
|
||||
"search.filters.reset": "Reset filters",
|
||||
@@ -1988,6 +2191,8 @@
|
||||
"title": "DSpace",
|
||||
|
||||
|
||||
"administrativeView.search.results.head": "Administrative Search",
|
||||
|
||||
|
||||
"uploader.browse": "browse",
|
||||
|
||||
|
@@ -1248,7 +1248,7 @@
|
||||
|
||||
|
||||
// "journalissue.listelement.badge": "Journal Issue",
|
||||
"journalissue.listelement.badge": "Tijdschrift aflevering",
|
||||
"journalissue.listelement.badge": "Tijdschriftaflevering",
|
||||
|
||||
// "journalissue.page.description": "Description",
|
||||
"journalissue.page.description": "Beschrijving",
|
||||
|
@@ -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 { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
let fixture: ComponentFixture<EPeopleRegistryComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
const mockEPeople = [EPersonMock, EPersonMock2];
|
||||
let ePersonDataServiceStub: any;
|
||||
|
||||
beforeEach(async(() => {
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return ePerson.email === query
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
if (scope === 'metadata') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
this.activeEPerson = ePerson;
|
||||
},
|
||||
cancelEditEPerson() {
|
||||
this.activeEPerson = null;
|
||||
},
|
||||
clearEPersonRequests(): void {
|
||||
// empty
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent],
|
||||
providers: [EPeopleRegistryComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should display list of ePeople', () => {
|
||||
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
expect(ePeopleIdsFound.length).toEqual(2);
|
||||
mockEPeople.map((ePerson: EPerson) => {
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
|
||||
})).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching with scope/query (scope metadata)', () => {
|
||||
let ePeopleIdsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'metadata', query: EPersonMock2.name });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching with scope/query (scope email)', () => {
|
||||
let ePeopleIdsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'email', query: EPersonMock.email });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEditEPerson', () => {
|
||||
describe('when you click on first edit eperson button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
||||
editButtons[0].triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('editEPerson form is toggled', () => {
|
||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||
expect(component.isEPersonFormShown).toEqual(false);
|
||||
} else {
|
||||
expect(component.isEPersonFormShown).toEqual(true);
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEPerson', () => {
|
||||
describe('when you click on first delete eperson button', () => {
|
||||
let ePeopleIdsFoundBeforeDelete;
|
||||
let ePeopleIdsFoundAfterDelete;
|
||||
beforeEach(fakeAsync(() => {
|
||||
ePeopleIdsFoundBeforeDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
|
||||
deleteButtons[0].triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFoundAfterDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('first ePerson is deleted', () => {
|
||||
expect(ePeopleIdsFoundBeforeDelete.length === ePeopleIdsFoundAfterDelete + 1);
|
||||
ePeopleIdsFoundAfterDelete.forEach((epersonElement) => {
|
||||
expect(epersonElement !== ePeopleIdsFoundBeforeDelete[0].nativeElement.textContent).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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 { EPersonMock } from '../../../shared/testing/eperson-mock';
|
||||
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
|
||||
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
|
||||
|
||||
const initialState: EPeopleRegistryState = {
|
||||
editEPerson: null,
|
||||
};
|
||||
|
||||
const editState: EPeopleRegistryState = {
|
||||
editEPerson: EPersonMock,
|
||||
};
|
||||
|
||||
class NullAction extends EPeopleRegistryEditEPersonAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('epeopleRegistryReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with an initial state', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const initState = ePeopleRegistryReducer(undefined, action);
|
||||
|
||||
expect(initState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should update the current state to change the editEPerson to a new eperson when EPeopleRegistryEditEPersonAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new EPeopleRegistryEditEPersonAction(EPersonMock);
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editEPerson).toEqual(EPersonMock);
|
||||
});
|
||||
|
||||
it('should update the current state to remove the editEPerson from the state when EPeopleRegistryCancelEPersonAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new EPeopleRegistryCancelEPersonAction();
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editEPerson).toEqual(null);
|
||||
});
|
||||
});
|
@@ -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 { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
|
||||
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||
import { EPersonFormComponent } from './eperson-form.component';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
let fixture: ComponentFixture<EPersonFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
const mockEPeople = [EPersonMock, EPersonMock2];
|
||||
let ePersonDataServiceStub: any;
|
||||
|
||||
beforeEach(async(() => {
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return ePerson.email === query
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
if (scope === 'metadata') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
},
|
||||
create(ePerson: EPerson) {
|
||||
this.allEpeople = [...this.allEpeople, ePerson]
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
this.activeEPerson = ePerson;
|
||||
},
|
||||
cancelEditEPerson() {
|
||||
this.activeEPerson = null;
|
||||
},
|
||||
clearEPersonRequests(): void {
|
||||
// empty
|
||||
},
|
||||
tryToCreate(ePerson: EPerson): Observable<RestResponse> {
|
||||
this.allEpeople = [...this.allEpeople, ePerson]
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
updateEPerson(ePerson: EPerson): Observable<RestResponse> {
|
||||
this.allEpeople.forEach((ePersonInList: EPerson, i: number) => {
|
||||
if (ePersonInList.id === ePerson.id) {
|
||||
this.allEpeople[i] = ePerson;
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
|
||||
providers: [EPersonFormComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
const firstName = 'testName';
|
||||
const lastName = 'testLastName';
|
||||
const email = 'testEmail@test.com';
|
||||
const canLogIn = false;
|
||||
const requireCertificate = false;
|
||||
|
||||
const expected = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: firstName
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: lastName
|
||||
},
|
||||
],
|
||||
},
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.firstName.value = firstName;
|
||||
component.lastName.value = lastName;
|
||||
component.email.value = email;
|
||||
component.canLogIn.value = canLogIn;
|
||||
component.requireCertificate.value = requireCertificate;
|
||||
});
|
||||
describe('without active EPerson', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new eperson using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active eperson', () => {
|
||||
const expectedWithId = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: firstName
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: lastName
|
||||
},
|
||||
],
|
||||
},
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit the existing eperson using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -2,8 +2,11 @@ import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getAdminModulePath } from '../app-routing.module';
|
||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
const REGISTRIES_MODULE_PATH = 'registries';
|
||||
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
|
||||
export function getRegistriesModulePath() {
|
||||
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||
@@ -15,7 +18,17 @@ export function getRegistriesModulePath() {
|
||||
{
|
||||
path: REGISTRIES_MODULE_PATH,
|
||||
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ACCESS_CONTROL_MODULE_PATH,
|
||||
loadChildren: './admin-access-control/admin-access-control.module#AdminAccessControlModule'
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AdminSearchPageComponent,
|
||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
||||
},
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -0,0 +1 @@
|
||||
<ds-configuration-search-page configuration="administrativeView" [context]="context"></ds-configuration-search-page>
|
@@ -0,0 +1,27 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminSearchPageComponent } from './admin-search-page.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('AdminSearchPageComponent', () => {
|
||||
let component: AdminSearchPageComponent;
|
||||
let fixture: ComponentFixture<AdminSearchPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AdminSearchPageComponent ],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminSearchPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-search-page',
|
||||
templateUrl: './admin-search-page.component.html',
|
||||
styleUrls: ['./admin-search-page.component.scss']
|
||||
})
|
||||
|
||||
/**
|
||||
* Component that represents a search page for administrators
|
||||
*/
|
||||
export class AdminSearchPageComponent {
|
||||
/**
|
||||
* The context of this page
|
||||
*/
|
||||
context: Context = Context.AdminSearch;
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<ds-collection-search-result-grid-element [object]="object"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item text-center">
|
||||
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-collection-search-result-grid-element>
|
||||
|
@@ -0,0 +1,66 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||
import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { CollectionAdminSearchResultGridElementComponent } from './collection-admin-search-result-grid-element.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
|
||||
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionAdminSearchResultGridElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new CollectionSearchResult();
|
||||
searchResult.indexableObject = new Collection();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([]),
|
||||
SharedModule
|
||||
],
|
||||
declarations: [CollectionAdminSearchResultGridElementComponent],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: BitstreamDataService, useValue: {} },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CollectionAdminSearchResultGridElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCollectionEditPath(id));
|
||||
})
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
|
||||
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-collection-admin-search-result-list-element',
|
||||
styleUrls: ['./collection-admin-search-result-grid-element.component.scss'],
|
||||
templateUrl: './collection-admin-search-result-grid-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for a collection search result on the admin search page
|
||||
*/
|
||||
export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> {
|
||||
editPath: string;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<ds-community-search-result-grid-element [object]="object"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item text-center">
|
||||
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-community-search-result-grid-element>
|
||||
|
@@ -0,0 +1,70 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||
import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||
|
||||
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunityAdminSearchResultGridElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new CommunitySearchResult();
|
||||
searchResult.indexableObject = new Community();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([]),
|
||||
SharedModule
|
||||
],
|
||||
declarations: [CommunityAdminSearchResultGridElementComponent],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: BitstreamDataService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommunityAdminSearchResultGridElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCommunityEditPath(id));
|
||||
})
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
|
||||
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-community-admin-search-result-grid-element',
|
||||
styleUrls: ['./community-admin-search-result-grid-element.component.scss'],
|
||||
templateUrl: './community-admin-search-result-grid-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for a community search result on the admin search page
|
||||
*/
|
||||
export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> {
|
||||
editPath: string;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<ng-template dsListableObject>
|
||||
</ng-template>
|
||||
<div #badges class="position-absolute ml-1">
|
||||
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul #buttons class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<ds-item-admin-search-result-actions-element class="d-flex justify-content-between" [item]="dso" [small]="true"></ds-item-admin-search-result-actions-element>
|
||||
</li>
|
||||
</ul>
|
@@ -0,0 +1,121 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||
import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
||||
|
||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
let component: ItemAdminSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<ItemAdminSearchResultGridElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
const mockBitstreamDataService = {
|
||||
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
||||
return createSuccessfulRemoteDataObject$(new Bitstream());
|
||||
}
|
||||
};
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new ItemSearchResult();
|
||||
searchResult.indexableObject = new Item();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule(
|
||||
{
|
||||
declarations: [ItemAdminSearchResultGridElementComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([]),
|
||||
SharedModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemAdminSearchResultGridElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should not show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
})
|
||||
});
|
@@ -0,0 +1,75 @@
|
||||
import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module';
|
||||
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||
import {
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
|
||||
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-item-admin-search-result-grid-element',
|
||||
styleUrls: ['./item-admin-search-result-grid-element.component.scss'],
|
||||
templateUrl: './item-admin-search-result-grid-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for an item search result on the admin search page
|
||||
*/
|
||||
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnInit {
|
||||
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||
|
||||
constructor(protected truncatableService: TruncatableService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver
|
||||
) {
|
||||
super(truncatableService, bitstreamDataService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the dynamic child component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
[this.badges.nativeElement],
|
||||
[this.buttons.nativeElement]
|
||||
]);
|
||||
(componentRef.instance as any).object = this.object;
|
||||
(componentRef.instance as any).index = this.index;
|
||||
(componentRef.instance as any).linkType = this.linkType;
|
||||
(componentRef.instance as any).listID = this.listID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's relationship type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
*/
|
||||
private getComponent(): GenericConstructor<Component> {
|
||||
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined)
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
<ds-collection-search-result-list-element [object]="object"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID"></ds-collection-search-result-list-element>
|
||||
<div>
|
||||
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
|
||||
<i class="fa fa-edit"></i> {{"admin.search.collection.edit" | translate}}
|
||||
</a>
|
||||
</div>
|
@@ -0,0 +1,60 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CollectionAdminSearchResultListElementComponent } from './collection-admin-search-result-list-element.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
|
||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<CollectionAdminSearchResultListElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new CollectionSearchResult();
|
||||
searchResult.indexableObject = new Collection();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CollectionAdminSearchResultListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCollectionEditPath(id));
|
||||
})
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
|
||||
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-collection-admin-search-result-list-element',
|
||||
styleUrls: ['./collection-admin-search-result-list-element.component.scss'],
|
||||
templateUrl: './collection-admin-search-result-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for a collection search result on the admin search page
|
||||
*/
|
||||
export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> {
|
||||
editPath: string;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
<ds-community-search-result-list-element [object]="object"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID"></ds-community-search-result-list-element>
|
||||
<div>
|
||||
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
|
||||
<i class="fa fa-edit"></i> {{"admin.search.community.edit" | translate}}
|
||||
</a>
|
||||
</div>
|
@@ -0,0 +1,60 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
|
||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<CommunityAdminSearchResultListElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new CommunitySearchResult();
|
||||
searchResult.indexableObject = new Community();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommunityAdminSearchResultListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCommunityEditPath(id));
|
||||
})
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
|
||||
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-community-admin-search-result-list-element',
|
||||
styleUrls: ['./community-admin-search-result-list-element.component.scss'],
|
||||
templateUrl: './community-admin-search-result-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for a community search result on the admin search page
|
||||
*/
|
||||
export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> {
|
||||
editPath: string;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
|
||||
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
|
||||
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
|
||||
</div>
|
||||
<ds-listable-object-component-loader [object]="object"
|
||||
[viewMode]="viewModes.ListElement"
|
||||
[index]="index"
|
||||
[linkType]="linkType"
|
||||
[listID]="listID"></ds-listable-object-component-loader>
|
||||
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>
|
@@ -0,0 +1,101 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
|
||||
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
let component: ItemAdminSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ItemAdminSearchResultListElementComponent>;
|
||||
let id;
|
||||
let searchResult;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
searchResult = new ItemSearchResult();
|
||||
searchResult.indexableObject = new Item();
|
||||
searchResult.indexableObject.uuid = id;
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [ItemAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemAdminSearchResultListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = searchResult;
|
||||
component.linkTypes = CollectionElementLinkType;
|
||||
component.index = 0;
|
||||
component.viewModes = ViewMode;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isWithdrawn = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the withdrawn badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should not show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
component.dso.isDiscoverable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the private badge', () => {
|
||||
const badge = fixture.debugElement.query(By.css('div.private-badge'));
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
})
|
||||
});
|
@@ -0,0 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
|
||||
@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||
@Component({
|
||||
selector: 'ds-item-admin-search-result-list-element',
|
||||
styleUrls: ['./item-admin-search-result-list-element.component.scss'],
|
||||
templateUrl: './item-admin-search-result-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying a list element for an item search result on the admin search page
|
||||
*/
|
||||
export class ItemAdminSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditPath()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawPath()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstatePath()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivatePath()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicPath()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeletePath()]" [title]="'admin.search.item.delete' | translate">
|
||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMovePath()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,144 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemAdminSearchResultActionsComponent } from './item-admin-search-result-actions.component';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import {
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
|
||||
describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
let component: ItemAdminSearchResultActionsComponent;
|
||||
let fixture: ComponentFixture<ItemAdminSearchResultActionsComponent>;
|
||||
let id;
|
||||
let item;
|
||||
|
||||
function init() {
|
||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||
item = new Item();
|
||||
item.uuid = id;
|
||||
}
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [ItemAdminSearchResultActionsComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemAdminSearchResultActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const button = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = button.nativeElement.href;
|
||||
expect(link).toContain(getItemEditPath(id));
|
||||
});
|
||||
|
||||
it('should render a delete button with the correct link', () => {
|
||||
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||
const link = button.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString());
|
||||
});
|
||||
|
||||
it('should render a move button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.move-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString());
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.item.isWithdrawn = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render a withdraw button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString());
|
||||
});
|
||||
|
||||
it('should not render a reinstate button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
|
||||
expect(a).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is withdrawn', () => {
|
||||
beforeEach(() => {
|
||||
component.item.isWithdrawn = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not render a withdraw button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
|
||||
expect(a).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a reinstate button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is not private', () => {
|
||||
beforeEach(() => {
|
||||
component.item.isDiscoverable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render a make private button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.private-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString());
|
||||
});
|
||||
|
||||
it('should not render a make public button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.public-link'));
|
||||
expect(a).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the item is private', () => {
|
||||
beforeEach(() => {
|
||||
component.item.isDiscoverable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not render a make private button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.private-link'));
|
||||
expect(a).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a make private button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.public-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString());
|
||||
});
|
||||
})
|
||||
});
|
@@ -0,0 +1,81 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import {
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-admin-search-result-actions-element',
|
||||
styleUrls: ['./item-admin-search-result-actions.component.scss'],
|
||||
templateUrl: './item-admin-search-result-actions.component.html'
|
||||
})
|
||||
/**
|
||||
* The component for displaying the actions for a list element for an item search result on the admin search page
|
||||
*/
|
||||
export class ItemAdminSearchResultActionsComponent {
|
||||
/**
|
||||
* The item to perform the actions on
|
||||
*/
|
||||
@Input() public item: Item;
|
||||
|
||||
/**
|
||||
* Whether or not to use small buttons
|
||||
*/
|
||||
@Input() public small: boolean;
|
||||
|
||||
/**
|
||||
* Returns the path to the edit page of this item
|
||||
*/
|
||||
getEditPath(): string {
|
||||
return getItemEditPath(this.item.uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the move page of this item
|
||||
*/
|
||||
getMovePath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_MOVE_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the delete page of this item
|
||||
*/
|
||||
getDeletePath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_DELETE_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the withdraw page of this item
|
||||
*/
|
||||
getWithdrawPath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_WITHDRAW_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the reinstate page of this item
|
||||
*/
|
||||
getReinstatePath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_REINSTATE_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the page where the user can make this item private
|
||||
*/
|
||||
getPrivatePath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PRIVATE_PATH).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the page where the user can make this item public
|
||||
*/
|
||||
getPublicPath(): string {
|
||||
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PUBLIC_PATH).toString();
|
||||
}
|
||||
}
|
@@ -1,23 +1,23 @@
|
||||
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 { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||
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 { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-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 {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
|
||||
@@ -325,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: ''
|
||||
link: '/admin/access-control/epeople'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -350,53 +350,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Search */
|
||||
/* Admin Search */
|
||||
{
|
||||
id: 'find',
|
||||
id: 'admin_search',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.find'
|
||||
} as TextMenuItemModel,
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.admin_search',
|
||||
link: '/admin/search'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'search',
|
||||
index: 5
|
||||
},
|
||||
{
|
||||
id: 'find_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_items',
|
||||
link: '/search'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'find_withdrawn_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_withdrawn_items',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'find_private_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_private_items',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Registries */
|
||||
{
|
||||
id: 'registries',
|
||||
|
@@ -1,14 +1,45 @@
|
||||
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 { AdminRoutingModule } from './admin-routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||
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 { CommunityAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||
import { CollectionAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component';
|
||||
import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component';
|
||||
import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
|
||||
import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
|
||||
import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AdminRegistriesModule,
|
||||
AdminRoutingModule,
|
||||
AdminRegistriesModule,
|
||||
AdminAccessControlModule,
|
||||
SharedModule,
|
||||
SearchPageModule
|
||||
],
|
||||
declarations: [
|
||||
AdminSearchPageComponent,
|
||||
ItemAdminSearchResultListElementComponent,
|
||||
CommunityAdminSearchResultListElementComponent,
|
||||
CollectionAdminSearchResultListElementComponent,
|
||||
ItemAdminSearchResultGridElementComponent,
|
||||
CommunityAdminSearchResultGridElementComponent,
|
||||
CollectionAdminSearchResultGridElementComponent,
|
||||
ItemAdminSearchResultActionsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
ItemAdminSearchResultListElementComponent,
|
||||
CommunityAdminSearchResultListElementComponent,
|
||||
CollectionAdminSearchResultListElementComponent,
|
||||
ItemAdminSearchResultGridElementComponent,
|
||||
CommunityAdminSearchResultGridElementComponent,
|
||||
CollectionAdminSearchResultGridElementComponent,
|
||||
ItemAdminSearchResultActionsComponent
|
||||
]
|
||||
})
|
||||
export class AdminModule {
|
||||
|
||||
|
@@ -40,6 +40,7 @@ const COLLECTION_EDIT_PATH = 'edit';
|
||||
dso: CollectionPageResolver,
|
||||
breadcrumb: CollectionBreadcrumbResolver
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: COLLECTION_EDIT_PATH,
|
||||
|
@@ -39,6 +39,7 @@ const COMMUNITY_EDIT_PATH = 'edit';
|
||||
dso: CommunityPageResolver,
|
||||
breadcrumb: CommunityBreadcrumbResolver
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: COMMUNITY_EDIT_PATH,
|
||||
|
@@ -22,6 +22,7 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi
|
||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -47,6 +48,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
|
||||
ItemMetadataComponent,
|
||||
ItemRelationshipsComponent,
|
||||
ItemBitstreamsComponent,
|
||||
ItemVersionHistoryComponent,
|
||||
EditInPlaceFieldComponent,
|
||||
EditRelationshipComponent,
|
||||
EditRelationshipListComponent,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ItemPageResolver } from '../item-page.resolver';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
@@ -14,13 +13,14 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col
|
||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
|
||||
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||
const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||
const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||
export const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||
export const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||
export const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||
export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||
@@ -75,6 +75,11 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
/* TODO - change when curate page exists */
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||
},
|
||||
{
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="mt-4">
|
||||
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
||||
</div>
|
||||
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
||||
</div>
|
@@ -0,0 +1,44 @@
|
||||
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
|
||||
|
||||
describe('ItemVersionHistoryComponent', () => {
|
||||
let component: ItemVersionHistoryComponent;
|
||||
let fixture: ComponentFixture<ItemVersionHistoryComponent>;
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
uuid: 'item-identifier-1',
|
||||
handle: '123456789/1',
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemVersionHistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should initialize the itemRD$ from the route\'s data', (done) => {
|
||||
component.itemRD$.subscribe((itemRD) => {
|
||||
expect(itemRD.payload).toBe(item);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,35 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-version-history',
|
||||
templateUrl: './item-version-history.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for listing and managing an item's version history
|
||||
*/
|
||||
export class ItemVersionHistoryComponent {
|
||||
/**
|
||||
* The item to display the version history for
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||
}
|
||||
}
|
@@ -21,6 +21,7 @@
|
||||
</table>
|
||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
|
@@ -30,6 +30,7 @@ const ITEM_EDIT_PATH = 'edit';
|
||||
item: ItemPageResolver,
|
||||
breadcrumb: ItemBreadcrumbResolver
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
||||
MetadataRepresentationListComponent,
|
||||
RelatedEntitiesSearchComponent,
|
||||
TabbedRelatedEntitiesSearchComponent,
|
||||
AbstractIncrementalListComponent
|
||||
AbstractIncrementalListComponent,
|
||||
],
|
||||
exports: [
|
||||
ItemComponent,
|
||||
|
@@ -28,6 +28,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||
followLink('owningCollection'),
|
||||
followLink('bundles'),
|
||||
followLink('relationships'),
|
||||
followLink('version', undefined, true, followLink('versionhistory')),
|
||||
).pipe(
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||
);
|
||||
|
@@ -3,6 +3,7 @@
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchComponent } from './search.component';
|
||||
|
@@ -14,6 +14,7 @@ import { SearchPageComponent } from './search-page.component';
|
||||
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
@@ -28,7 +29,7 @@ const components = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CoreModule.forRoot(),
|
||||
StatisticsModule.forRoot(),
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: components,
|
||||
providers: [
|
||||
|
@@ -1,54 +1,55 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"
|
||||
[context]="context"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<div class="row mb-3 mb-md-1">
|
||||
<div class="labels col-sm-9 offset-sm-3">
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<div class="row mb-3 mb-md-1">
|
||||
<div class="labels col-sm-9 offset-sm-3">
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -17,6 +17,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { currentPath } from '../shared/utils/route.utils';
|
||||
import { Router } from '@angular/router';
|
||||
import { Context } from '../core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search',
|
||||
@@ -84,6 +85,12 @@ export class SearchComponent implements OnInit {
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The current context
|
||||
*/
|
||||
@Input()
|
||||
context: Context;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
|
@@ -3,7 +3,6 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
import { Breadcrumb } from './breadcrumbs/breadcrumb/breadcrumb.model';
|
||||
import { DSpaceObject } from './core/shared/dspace-object.model';
|
||||
import { Community } from './core/shared/community.model';
|
||||
import { getCommunityPageRoute } from './+community-page/community-page-routing.module';
|
||||
@@ -11,7 +10,6 @@ import { Collection } from './core/shared/collection.model';
|
||||
import { Item } from './core/shared/item.model';
|
||||
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
||||
import { BrowseByDSOBreadcrumbResolver } from './+browse-by/browse-by-dso-breadcrumb.resolver';
|
||||
|
||||
const ITEM_MODULE_PATH = 'items';
|
||||
|
||||
@@ -37,6 +35,12 @@ export function getAdminModulePath() {
|
||||
return `/${ADMIN_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
const PROFILE_MODULE_PATH = 'profile';
|
||||
|
||||
export function getProfileModulePath() {
|
||||
return `/${PROFILE_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export function getDSOPath(dso: DSpaceObject): string {
|
||||
switch ((dso as any).type) {
|
||||
case Community.type.value:
|
||||
@@ -78,8 +82,15 @@ export function getDSOPath(dso: DSpaceObject): string {
|
||||
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 },
|
||||
])
|
||||
],
|
||||
{
|
||||
onSameUrlNavigation: 'reload',
|
||||
})
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
@@ -1,30 +1,34 @@
|
||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||
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 { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import {
|
||||
ePeopleRegistryReducer,
|
||||
EPeopleRegistryState
|
||||
} from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
|
||||
import {
|
||||
metadataRegistryReducer,
|
||||
MetadataRegistryState
|
||||
} 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 { 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 { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
||||
import {
|
||||
selectableListReducer,
|
||||
SelectableListsState
|
||||
} from './shared/object-list/selectable-list/selectable-list.reducer';
|
||||
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
||||
import {
|
||||
NameVariantListsState,
|
||||
nameVariantReducer
|
||||
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||
|
||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.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 {
|
||||
router: fromRouter.RouterReducerState;
|
||||
@@ -42,6 +46,7 @@ export interface AppState {
|
||||
selectableLists: SelectableListsState;
|
||||
relationshipLists: NameVariantListsState;
|
||||
communityList: CommunityListState;
|
||||
epeopleRegistry: EPeopleRegistryState;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
@@ -60,6 +65,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
selectableLists: selectableListReducer,
|
||||
relationshipLists: nameVariantReducer,
|
||||
communityList: CommunityListReducer,
|
||||
epeopleRegistry: ePeopleRegistryReducer,
|
||||
};
|
||||
|
||||
export const routerStateSelector = (state: AppState) => state.router;
|
||||
|
@@ -27,7 +27,7 @@ import { NativeWindowRef, NativeWindowService } from '../services/window.service
|
||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
@@ -168,7 +168,7 @@ export class AuthService {
|
||||
*/
|
||||
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
||||
return this.epersonService.findByHref(userHref).pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
getAllSucceededRemoteDataPayload()
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collecti
|
||||
*/
|
||||
get followLinks(): Array<FollowLinkConfig<Collection>> {
|
||||
return [
|
||||
followLink('parentCommunity', undefined,
|
||||
followLink('parentCommunity', undefined, true,
|
||||
followLink('parentCommunity')
|
||||
)
|
||||
];
|
||||
|
@@ -21,8 +21,8 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
||||
*/
|
||||
get followLinks(): Array<FollowLinkConfig<Item>> {
|
||||
return [
|
||||
followLink('owningCollection', undefined,
|
||||
followLink('parentCommunity', undefined,
|
||||
followLink('owningCollection', undefined, true,
|
||||
followLink('parentCommunity', undefined, true,
|
||||
followLink('parentCommunity'))
|
||||
),
|
||||
followLink('bundles'),
|
||||
|
10
src/app/core/cache/builders/link.service.spec.ts
vendored
10
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -90,7 +90,7 @@ describe('LinkService', () => {
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor')))
|
||||
});
|
||||
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
||||
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, followLink('successor'));
|
||||
@@ -105,7 +105,7 @@ describe('LinkService', () => {
|
||||
isList: true
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options '} as any, followLink('successor')))
|
||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options '} as any, true, followLink('successor')))
|
||||
});
|
||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
||||
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options '} as any, followLink('successor'));
|
||||
@@ -119,7 +119,7 @@ describe('LinkService', () => {
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
result = service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor')))
|
||||
});
|
||||
|
||||
it('should call getLinkDefinition with the correct model and link', () => {
|
||||
@@ -144,7 +144,7 @@ describe('LinkService', () => {
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor')))
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -160,7 +160,7 @@ describe('LinkService', () => {
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')))
|
||||
service.resolveLink(testModel, followLink('predecessor', {}, true, followLink('successor')))
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
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 { selfLinkFromUuidSelector } from '../index/index.selectors';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { getClassForType } from './builders/build-decorators';
|
||||
import { LinkService } from './builders/link.service';
|
||||
import {
|
||||
AddPatchObjectCacheAction,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
|
||||
import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
|
||||
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
|
||||
@@ -48,7 +48,7 @@ export class ObjectCacheService {
|
||||
constructor(
|
||||
private store: Store<CoreState>,
|
||||
private linkService: LinkService
|
||||
) {
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -51,6 +51,14 @@ describe('ServerSyncBufferEffects', () => {
|
||||
_links: { self: { href: link } }
|
||||
});
|
||||
return observableOf(object);
|
||||
},
|
||||
getBySelfLink: (link) => {
|
||||
const object = Object.assign(new DSpaceObject(), {
|
||||
_links: {
|
||||
self: { href: link }
|
||||
}
|
||||
});
|
||||
return observableOf(object);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
22
src/app/core/cache/server-sync-buffer.effects.ts
vendored
22
src/app/core/cache/server-sync-buffer.effects.ts
vendored
@@ -16,13 +16,15 @@ import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/s
|
||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
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 { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { ObjectCacheEntry } from './object-cache.reducer';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
@Injectable()
|
||||
export class ServerSyncBufferEffects {
|
||||
@@ -96,17 +98,19 @@ export class ServerSyncBufferEffects {
|
||||
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||
*/
|
||||
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(
|
||||
map((object) => {
|
||||
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||
|
||||
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
||||
|
||||
return new ApplyPatchObjectCacheAction(href)
|
||||
map((entry: ObjectCacheEntry) => {
|
||||
if (isNotEmpty(entry.patches)) {
|
||||
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||
if (isNotEmpty(flatPatch)) {
|
||||
this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, flatPatch));
|
||||
}
|
||||
}
|
||||
return new ApplyPatchObjectCacheAction(href);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(private actions$: Actions,
|
||||
|
@@ -135,6 +135,10 @@ import { PoolTask } from './tasks/models/pool-task-object.model';
|
||||
import { TaskObject } from './tasks/models/task-object.model';
|
||||
import { PoolTaskDataService } from './tasks/pool-task-data.service';
|
||||
import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
||||
import { VersionDataService } from './data/version-data.service';
|
||||
import { VersionHistoryDataService } from './data/version-history-data.service';
|
||||
import { Version } from './shared/version.model';
|
||||
import { VersionHistory } from './shared/version-history.model';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -251,6 +255,8 @@ const PROVIDERS = [
|
||||
RelationshipTypeService,
|
||||
ExternalSourceService,
|
||||
LookupRelationService,
|
||||
VersionDataService,
|
||||
VersionHistoryDataService,
|
||||
LicenseDataService,
|
||||
ItemTypeDataService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
@@ -301,6 +307,8 @@ export const models =
|
||||
ItemType,
|
||||
ExternalSource,
|
||||
ExternalSourceEntry,
|
||||
Version,
|
||||
VersionHistory
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
104
src/app/core/data/base-response-parsing.service.spec.ts
Normal file
104
src/app/core/data/base-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { GetRequest, RestRequest } from './request.models';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class TestService extends BaseResponseParsingService {
|
||||
toCache = true;
|
||||
|
||||
constructor(protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Overwrite methods to make them public for testing
|
||||
public process<ObjectDomain>(data: any, request: RestRequest): any {
|
||||
super.process(data, request);
|
||||
}
|
||||
|
||||
public cache<ObjectDomain>(obj, request: RestRequest, data: any) {
|
||||
super.cache(obj, request, data);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseResponseParsingService', () => {
|
||||
let service: TestService;
|
||||
let config: GlobalConfig;
|
||||
let objectCache: ObjectCacheService;
|
||||
|
||||
const requestUUID = 'request-uuid';
|
||||
const requestHref = 'request-href';
|
||||
const request = new GetRequest(requestUUID, requestHref);
|
||||
|
||||
beforeEach(() => {
|
||||
config = Object.assign({});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
add: {}
|
||||
});
|
||||
service = new TestService(config, objectCache);
|
||||
});
|
||||
|
||||
describe('cache', () => {
|
||||
let obj: CacheableObject;
|
||||
|
||||
describe('when the object is undefined', () => {
|
||||
it('should not throw an error', () => {
|
||||
expect(() => { service.cache(obj, request, {}) }).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not call objectCache add', () => {
|
||||
service.cache(obj, request, {});
|
||||
expect(objectCache.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the object has a self link', () => {
|
||||
beforeEach(() => {
|
||||
obj = Object.assign(new DSpaceObject(), {
|
||||
_links: {
|
||||
self: { href: 'obj-selflink' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call objectCache add', () => {
|
||||
service.cache(obj, request, {});
|
||||
expect(objectCache.add).toHaveBeenCalledWith(obj, request.responseMsToLive, request.uuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('process', () => {
|
||||
let data: any;
|
||||
let result: any;
|
||||
|
||||
describe('when data is valid, but not a real type', () => {
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
type: 'NotARealType',
|
||||
_links: {
|
||||
self: { href: 'data-selflink' }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should not throw an error', () => {
|
||||
expect(() => { result = service.process(data, request) }).not.toThrow();
|
||||
});
|
||||
|
||||
it('should return undefined', () => {
|
||||
result = service.process(data, request);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not call objectCache add', () => {
|
||||
result = service.process(data, request);
|
||||
expect(objectCache.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
/* tslint:enable:max-classes-per-file */
|
@@ -57,7 +57,7 @@ export abstract class BaseResponseParsingService {
|
||||
.filter((property) => data._embedded.hasOwnProperty(property))
|
||||
.forEach((property) => {
|
||||
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])) {
|
||||
object[property] = parsedObj;
|
||||
object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj));
|
||||
@@ -117,10 +117,12 @@ export abstract class BaseResponseParsingService {
|
||||
const serializer = new this.serializerConstructor(objConstructor);
|
||||
return serializer.deserialize(obj);
|
||||
} else {
|
||||
console.warn('cannot deserialize type ' + type);
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
console.warn('cannot deserialize type ' + type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +144,8 @@ export abstract class BaseResponseParsingService {
|
||||
} else {
|
||||
dataJSON = JSON.stringify(data);
|
||||
}
|
||||
throw new Error(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`);
|
||||
console.warn(`Can't cache incomplete ${type}: ${JSON.stringify(co)}, parsed from (partial) response: ${dataJSON}`);
|
||||
return;
|
||||
}
|
||||
this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : this.EnvConfig.cache.msToLive.default, request.uuid);
|
||||
}
|
||||
|
@@ -1,21 +1,25 @@
|
||||
import { DataService } from './data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FindListOptions } from './request.models';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { DataService } from './data.service';
|
||||
import { FindListOptions, PatchRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
@@ -40,6 +44,7 @@ class TestService extends DataService<any> {
|
||||
return observableOf(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||
diff(object1: Item, object2: Item): Operation[] {
|
||||
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||
@@ -50,8 +55,8 @@ class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||
describe('DataService', () => {
|
||||
let service: TestService;
|
||||
let options: FindListOptions;
|
||||
const requestService = {generateRequestId: () => uuidv4()} as RequestService;
|
||||
const halService = {} as HALEndpointService;
|
||||
const requestService = getMockRequestService();
|
||||
const halService = new HALEndpointServiceStub('url') as any;
|
||||
const rdbService = {} as RemoteDataBuildService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
@@ -144,21 +149,161 @@ describe('DataService', () => {
|
||||
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
describe('patch', () => {
|
||||
let operations;
|
||||
let selfLink;
|
||||
|
||||
beforeEach(() => {
|
||||
operations = [{ op: 'replace', path: '/metadata/dc.title', value: 'random string' } as Operation];
|
||||
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', () => {
|
||||
service.patch(selfLink, operations);
|
||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
||||
it('should include single linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const expected = `${endpoint}?embed=bundles`;
|
||||
|
||||
(service as any).getFindAllHref({}, null, mockFollowLinkConfig).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include multiple linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||
|
||||
(service as any).getFindAllHref({}, null, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${endpoint}?embed=templateItemOf`;
|
||||
|
||||
(service as any).getFindAllHref({}, null, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'relationships' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Collection> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'itemtemplate' as any,
|
||||
linksToFollow: mockFollowLinkConfig3,
|
||||
});
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
linksToFollow: mockFollowLinkConfig2,
|
||||
});
|
||||
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
||||
|
||||
(service as any).getFindAllHref({}, null, mockFollowLinkConfig).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIDHref', () => {
|
||||
const endpointMock = 'https://dspace7-internal.atmire.com/server/api/core/items';
|
||||
const resourceIdMock = '003c99b4-d4fe-44b0-a945-e12182a7ca89';
|
||||
|
||||
it('should return endpoint', () => {
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock);
|
||||
expect(result).toEqual(endpointMock + '/' + resourceIdMock);
|
||||
});
|
||||
|
||||
it('should include single linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles`;
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should include multiple linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'relationships' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Collection> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'itemtemplate' as any,
|
||||
linksToFollow: mockFollowLinkConfig3,
|
||||
});
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
linksToFollow: mockFollowLinkConfig2,
|
||||
});
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, mockFollowLinkConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch', () => {
|
||||
const dso = {
|
||||
uuid: 'dso-uuid'
|
||||
};
|
||||
const operations = [
|
||||
Object.assign({
|
||||
op: 'move',
|
||||
from: '/1',
|
||||
path: '/5'
|
||||
}) as Operation
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
service.patch(dso, operations);
|
||||
});
|
||||
|
||||
it('should configure a PatchRequest', () => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PatchRequest));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -44,7 +44,7 @@ import {
|
||||
FindByIDRequest,
|
||||
FindListOptions,
|
||||
FindListRequest,
|
||||
GetRequest
|
||||
GetRequest, PatchRequest
|
||||
} from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RequestService } from './request.service';
|
||||
@@ -83,14 +83,15 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param linkPath The link path for the object
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
protected getFindAllHref(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
protected getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
||||
let result$: Observable<string>;
|
||||
const args = [];
|
||||
|
||||
result$ = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged());
|
||||
|
||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args)));
|
||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,8 +101,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
protected getSearchByHref(searchMethod: string, options: FindListOptions = {}): Observable<string> {
|
||||
protected getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
||||
let result$: Observable<string>;
|
||||
const args = [];
|
||||
|
||||
@@ -113,7 +115,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
})
|
||||
}
|
||||
|
||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args)));
|
||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,9 +126,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param extraArgs Array with additional params to combine with query string
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = []): string {
|
||||
|
||||
protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||
let args = [...extraArgs];
|
||||
|
||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||
@@ -142,6 +144,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
if (hasValue(options.startsWith)) {
|
||||
args = [...args, `startsWith=${options.startsWith}`];
|
||||
}
|
||||
args = this.addEmbedParams(args, ...linksToFollow);
|
||||
if (isNotEmpty(args)) {
|
||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||
} else {
|
||||
@@ -149,6 +152,40 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the embed options to the link for the request
|
||||
* @param args params for the query string
|
||||
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||
*/
|
||||
protected addEmbedParams(args: string[], ...linksToFollow: Array<FollowLinkConfig<T>>) {
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
if (linkToFollow !== undefined && linkToFollow.shouldEmbed) {
|
||||
const embedString = 'embed=' + String(linkToFollow.name);
|
||||
const embedWithNestedString = this.addNestedEmbeds(embedString, ...linkToFollow.linksToFollow);
|
||||
args = [...args, embedWithNestedString];
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the nested followLinks to the embed param, recursively, separated by a /
|
||||
* @param embedString embedString so far (recursive)
|
||||
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||
*/
|
||||
protected addNestedEmbeds(embedString: string, ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||
let nestEmbed = embedString;
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
if (linkToFollow !== undefined && linkToFollow.shouldEmbed) {
|
||||
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
|
||||
if (linkToFollow.linksToFollow !== undefined) {
|
||||
nestEmbed = this.addNestedEmbeds(nestEmbed, ...linkToFollow.linksToFollow);
|
||||
}
|
||||
}
|
||||
});
|
||||
return nestEmbed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||
* info should be added to the objects
|
||||
@@ -184,12 +221,13 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the HREF for a specific object based on its identifier
|
||||
* Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow
|
||||
* @param endpoint The base endpoint for the type of object
|
||||
* @param resourceID The identifier for the object
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
getIDHref(endpoint, resourceID): string {
|
||||
return `${endpoint}/${resourceID}`;
|
||||
getIDHref(endpoint, resourceID, ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||
return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,9 +237,8 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id))));
|
||||
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id), ...linksToFollow)));
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)))
|
||||
@@ -223,7 +260,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
|
||||
const requestHref = this.buildHrefFromFindOptions(href, {}, []);
|
||||
const requestHref = this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow);
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), requestHref);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
@@ -240,7 +277,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
||||
const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []);
|
||||
const requestHref = this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow);
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), requestHref);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
@@ -269,9 +306,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||
* 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);
|
||||
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||
|
||||
return hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
@@ -292,12 +329,28 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new patch to the object cache to a specified object
|
||||
* @param {string} href The selflink of the object that will be patched
|
||||
* Send a patch request for a specified object
|
||||
* @param {T} dso The object to send a patch request for
|
||||
* @param {Operation[]} operations The patch operations to be performed
|
||||
*/
|
||||
patch(href: string, operations: Operation[]) {
|
||||
this.objectCache.addPatch(href, operations);
|
||||
patch(dso: T, operations: Operation[]): Observable<RestResponse> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,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
|
||||
* @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 { Injectable } from '@angular/core';
|
||||
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
|
||||
@@ -21,6 +23,21 @@ export class DSOChangeAnalyzer<T extends DSpaceObject> implements ChangeAnalyzer
|
||||
* The second object to compare
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { DsoRedirectDataService } from './dso-redirect-data.service';
|
||||
import { FindByIDRequest, IdentifierType } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DsoRedirectDataService } from './dso-redirect-data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
|
||||
describe('DsoRedirectDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -148,5 +151,71 @@ describe('DsoRedirectDataService', () => {
|
||||
scheduler.flush();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['communities/' + remoteData.payload.uuid]);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('getIDHref', () => {
|
||||
it('should return endpoint', () => {
|
||||
const result = (service as any).getIDHref(pidLink, dsoUUID);
|
||||
expect(result).toEqual(requestUUIDURL);
|
||||
});
|
||||
|
||||
it('should include single linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const expected = `${requestUUIDURL}&embed=bundles`;
|
||||
const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should include multiple linksToFollow as embed', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${requestUUIDURL}&embed=bundles&embed=owningCollection&embed=templateItemOf`;
|
||||
const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'bundles' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
shouldEmbed: false,
|
||||
});
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'templateItemOf' as any,
|
||||
});
|
||||
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
||||
const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig, mockFollowLinkConfig2, mockFollowLinkConfig3);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const mockFollowLinkConfig3: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'relationships' as any,
|
||||
});
|
||||
const mockFollowLinkConfig2: FollowLinkConfig<Collection> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'itemtemplate' as any,
|
||||
linksToFollow: mockFollowLinkConfig3,
|
||||
});
|
||||
const mockFollowLinkConfig: FollowLinkConfig<Item> = Object.assign(new FollowLinkConfig(), {
|
||||
name: 'owningCollection' as any,
|
||||
linksToFollow: mockFollowLinkConfig2,
|
||||
});
|
||||
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
||||
const result = (service as any).getIDHref(pidLink, dsoUUID, mockFollowLinkConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -6,6 +6,7 @@ import { Observable } from 'rxjs';
|
||||
import { take, tap } from 'rxjs/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
@@ -45,10 +46,11 @@ export class DsoRedirectDataService extends DataService<any> {
|
||||
}
|
||||
}
|
||||
|
||||
getIDHref(endpoint, resourceID): string {
|
||||
getIDHref(endpoint, resourceID, ...linksToFollow: Array<FollowLinkConfig<any>>): string {
|
||||
// Supporting both identifier (pid) and uuid (dso) endpoints
|
||||
return endpoint.replace(/\{\?id\}/, `?id=${resourceID}`)
|
||||
.replace(/\{\?uuid\}/, `?uuid=${resourceID}`);
|
||||
return this.buildHrefFromFindOptions( endpoint.replace(/\{\?id\}/, `?id=${resourceID}`)
|
||||
.replace(/\{\?uuid\}/, `?uuid=${resourceID}`),
|
||||
{}, [], ...linksToFollow);
|
||||
}
|
||||
|
||||
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
|
||||
|
@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
@@ -31,8 +32,9 @@ class DataServiceImpl extends DataService<DSpaceObject> {
|
||||
super();
|
||||
}
|
||||
|
||||
getIDHref(endpoint, resourceID): string {
|
||||
return endpoint.replace(/\{\?uuid\}/, `?uuid=${resourceID}`);
|
||||
getIDHref(endpoint, resourceID, ...linksToFollow: Array<FollowLinkConfig<DSpaceObject>>): string {
|
||||
return this.buildHrefFromFindOptions( endpoint.replace(/\{\?uuid\}/, `?uuid=${resourceID}`),
|
||||
{}, [], ...linksToFollow);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -20,7 +20,7 @@ import { ITEM } from '../shared/item.resource-type';
|
||||
import {
|
||||
configureRequest,
|
||||
filterSuccessfulResponses,
|
||||
getRequestFromRequestHref,
|
||||
getRequestFromRequestHref, getRequestFromRequestUUID,
|
||||
getResponseFromEntry
|
||||
} from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
@@ -180,14 +180,17 @@ export class ItemDataService extends DataService<Item> {
|
||||
const patchOperation = [{
|
||||
op: 'replace', path: '/withdrawn', value: withdrawn
|
||||
}];
|
||||
this.requestService.removeByHrefSubstring('/discover');
|
||||
|
||||
return this.getItemWithdrawEndpoint(itemId).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) =>
|
||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||
),
|
||||
configureRequest(this.requestService),
|
||||
map((request: RestRequest) => request.href),
|
||||
getRequestFromRequestHref(this.requestService),
|
||||
map((request: RestRequest) => request.uuid),
|
||||
getRequestFromRequestUUID(this.requestService),
|
||||
filter((requestEntry: RequestEntry) => requestEntry.completed),
|
||||
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||
);
|
||||
}
|
||||
@@ -201,14 +204,17 @@ export class ItemDataService extends DataService<Item> {
|
||||
const patchOperation = [{
|
||||
op: 'replace', path: '/discoverable', value: discoverable
|
||||
}];
|
||||
this.requestService.removeByHrefSubstring('/discover');
|
||||
|
||||
return this.getItemDiscoverableEndpoint(itemId).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) =>
|
||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||
),
|
||||
configureRequest(this.requestService),
|
||||
map((request: RestRequest) => request.href),
|
||||
getRequestFromRequestHref(this.requestService),
|
||||
map((request: RestRequest) => request.uuid),
|
||||
getRequestFromRequestUUID(this.requestService),
|
||||
filter((requestEntry: RequestEntry) => requestEntry.completed),
|
||||
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||
);
|
||||
}
|
||||
|
@@ -119,6 +119,8 @@ export class HeadRequest extends RestRequest {
|
||||
}
|
||||
|
||||
export class PatchRequest extends RestRequest {
|
||||
public responseMsToLive = 60 * 15 * 1000;
|
||||
|
||||
constructor(
|
||||
public uuid: string,
|
||||
public href: string,
|
||||
|
@@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { Observable, race as observableRace } from 'rxjs';
|
||||
import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import { filter, map, mergeMap, take } from 'rxjs/operators';
|
||||
import { cloneDeep, remove } from 'lodash';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
|
44
src/app/core/data/version-data.service.ts
Normal file
44
src/app/core/data/version-data.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DataService } from './data.service';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION } from '../shared/version.resource-type';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Version object
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(VERSION)
|
||||
export class VersionDataService extends DataService<Version> {
|
||||
protected linkPath = 'versions';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<Version>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing versions
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
}
|
54
src/app/core/data/version-history-data.service.spec.ts
Normal file
54
src/app/core/data/version-history-data.service.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { VersionHistoryDataService } from './version-history-data.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { GetRequest } from './request.models';
|
||||
|
||||
const url = 'fake-url';
|
||||
|
||||
describe('VersionHistoryDataService', () => {
|
||||
let service: VersionHistoryDataService;
|
||||
|
||||
let requestService: RequestService;
|
||||
let notificationsService: any;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
|
||||
describe('getVersions', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = service.getVersions('1');
|
||||
});
|
||||
|
||||
it('should configure a GET request', () => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a VersionHistoryDataService used for testing
|
||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
||||
*/
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList')
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
halService = new HALEndpointServiceStub(url);
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, null, null);
|
||||
}
|
||||
});
|
81
src/app/core/data/version-history-data.service.ts
Normal file
81
src/app/core/data/version-history-data.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { DataService } from './data.service';
|
||||
import { VersionHistory } from '../shared/version-history.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions, GetRequest } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the VersionHistory object
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(VERSION_HISTORY)
|
||||
export class VersionHistoryDataService extends DataService<VersionHistory> {
|
||||
protected linkPath = 'versionhistories';
|
||||
protected versionsEndpoint = 'versions';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<VersionHistory>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing versions
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the versions endpoint for a version history
|
||||
* @param versionHistoryId
|
||||
*/
|
||||
getVersionsEndpoint(versionHistoryId: string): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.versionsEndpoint, `${href}/${versionHistoryId}`))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a version history's versions using paginated search options
|
||||
* @param versionHistoryId The version history's ID
|
||||
* @param searchOptions The search options to use
|
||||
* @param linksToFollow HAL Links to follow on the Versions
|
||||
*/
|
||||
getVersions(versionHistoryId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Version>>): Observable<RemoteData<PaginatedList<Version>>> {
|
||||
const hrefObs = this.getVersionsEndpoint(versionHistoryId).pipe(
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
hrefObs.pipe(
|
||||
take(1)
|
||||
).subscribe((href) => {
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildList<Version>(hrefObs, ...linksToFollow);
|
||||
}
|
||||
}
|
304
src/app/core/eperson/eperson-data.service.spec.ts
Normal file
304
src/app/core/eperson/eperson-data.service.spec.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import {
|
||||
EPeopleRegistryCancelEPersonAction,
|
||||
EPeopleRegistryEditEPersonAction
|
||||
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { DeleteByIDRequest, FindListOptions, PatchRequest } from '../data/request.models';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { EPersonDataService } from './eperson-data.service';
|
||||
import { EPerson } from './models/eperson.model';
|
||||
|
||||
describe('EPersonDataService', () => {
|
||||
let service: EPersonDataService;
|
||||
let store: Store<CoreState>;
|
||||
let requestService: RequestService;
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
const epeople = [EPersonMock, EPersonMock2];
|
||||
|
||||
const restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
|
||||
const epersonsEndpoint = `${restEndpointURL}/epersons`;
|
||||
let halService: any = new HALEndpointServiceStub(restEndpointURL);
|
||||
const epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople]));
|
||||
const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ });
|
||||
const objectCache = Object.assign({
|
||||
/* tslint:disable:no-empty */
|
||||
remove: () => {
|
||||
},
|
||||
hasBySelfLinkObservable: () => observableOf(false)
|
||||
/* tslint:enable:no-empty */
|
||||
}) as ObjectCacheService;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot({}),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
const getRequestEntry$ = (successful: boolean) => {
|
||||
return observableOf({
|
||||
completed: true,
|
||||
response: { isSuccessful: successful, payload: epeople } as any
|
||||
} as RequestEntry)
|
||||
};
|
||||
|
||||
function initTestService() {
|
||||
return new EPersonDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
null,
|
||||
halService,
|
||||
null,
|
||||
null,
|
||||
new DummyChangeAnalyzer() as any
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
service = initTestService();
|
||||
spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
describe('searchByScope', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'searchBy');
|
||||
});
|
||||
|
||||
it('search by default scope (byMetadata) and no query', () => {
|
||||
service.searchByScope(null, '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
|
||||
it('search metadata scope and no query', () => {
|
||||
service.searchByScope('metadata', '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
|
||||
it('search metadata scope and with query', () => {
|
||||
service.searchByScope('metadata', 'test');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', 'test'))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
|
||||
it('search email scope and no query', () => {
|
||||
service.searchByScope('email', '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('email', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byEmail', options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateEPerson', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));
|
||||
});
|
||||
|
||||
describe('change Email', () => {
|
||||
const newEmail = 'changedemail@test.com';
|
||||
beforeEach(() => {
|
||||
const changedEPerson = Object.assign(new EPerson(), {
|
||||
id: EPersonMock.id,
|
||||
metadata: EPersonMock.metadata,
|
||||
email: newEmail,
|
||||
canLogIn: EPersonMock.canLogIn,
|
||||
requireCertificate: EPersonMock.requireCertificate,
|
||||
_links: EPersonMock._links,
|
||||
});
|
||||
service.updateEPerson(changedEPerson).subscribe();
|
||||
});
|
||||
it('should send PatchRequest with replace email operation', () => {
|
||||
const operations = [{ op: 'replace', path: '/email', value: newEmail }];
|
||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change certificate', () => {
|
||||
beforeEach(() => {
|
||||
const changedEPerson = Object.assign(new EPerson(), {
|
||||
id: EPersonMock.id,
|
||||
metadata: EPersonMock.metadata,
|
||||
email: EPersonMock.email,
|
||||
canLogIn: EPersonMock.canLogIn,
|
||||
requireCertificate: !EPersonMock.requireCertificate,
|
||||
_links: EPersonMock._links,
|
||||
});
|
||||
service.updateEPerson(changedEPerson).subscribe();
|
||||
});
|
||||
it('should send PatchRequest with replace certificate operation', () => {
|
||||
const operations = [{ op: 'replace', path: '/certificate', value: !EPersonMock.requireCertificate }];
|
||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change canLogin', () => {
|
||||
beforeEach(() => {
|
||||
const changedEPerson = Object.assign(new EPerson(), {
|
||||
id: EPersonMock.id,
|
||||
metadata: EPersonMock.metadata,
|
||||
email: EPersonMock.email,
|
||||
canLogIn: !EPersonMock.canLogIn,
|
||||
requireCertificate: EPersonMock.requireCertificate,
|
||||
_links: EPersonMock._links,
|
||||
});
|
||||
service.updateEPerson(changedEPerson).subscribe();
|
||||
});
|
||||
it('should send PatchRequest with replace canLogIn operation', () => {
|
||||
const operations = [{ op: 'replace', path: '/canLogIn', value: !EPersonMock.canLogIn }];
|
||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change name', () => {
|
||||
const newFirstName = 'changedFirst';
|
||||
const newLastName = 'changedLast';
|
||||
beforeEach(() => {
|
||||
const changedEPerson = Object.assign(new EPerson(), {
|
||||
id: EPersonMock.id,
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: newFirstName,
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: newLastName,
|
||||
},
|
||||
],
|
||||
},
|
||||
email: EPersonMock.email,
|
||||
canLogIn: EPersonMock.canLogIn,
|
||||
requireCertificate: EPersonMock.requireCertificate,
|
||||
_links: EPersonMock._links,
|
||||
});
|
||||
service.updateEPerson(changedEPerson).subscribe();
|
||||
});
|
||||
it('should send PatchRequest with replace name metadata operations', () => {
|
||||
const operations = [
|
||||
{ op: 'replace', path: '/eperson.lastname/0/value', value: newLastName },
|
||||
{ op: 'replace', path: '/eperson.firstname/0/value', value: newFirstName }];
|
||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearEPersonRequests', () => {
|
||||
beforeEach(async(() => {
|
||||
scheduler = getTestScheduler();
|
||||
halService = {
|
||||
getEndpoint(linkPath: string): Observable<string> {
|
||||
return observableOf(restEndpointURL + '/' + linkPath);
|
||||
}
|
||||
} as HALEndpointService;
|
||||
initTestService();
|
||||
service.clearEPersonRequests();
|
||||
}));
|
||||
it('should remove the eperson hrefs in the request service', () => {
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(epersonsEndpoint);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveEPerson', () => {
|
||||
it('should retrieve the ePerson currently getting edited, if any', () => {
|
||||
service.editEPerson(EPersonMock);
|
||||
|
||||
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
expect(activeEPerson).toEqual(EPersonMock);
|
||||
})
|
||||
});
|
||||
|
||||
it('should retrieve the ePerson currently getting edited, null if none being edited', () => {
|
||||
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
expect(activeEPerson).toEqual(null);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('cancelEditEPerson', () => {
|
||||
it('should dispatch a CANCEL_EDIT_EPERSON action', () => {
|
||||
service.cancelEditEPerson();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new EPeopleRegistryCancelEPersonAction());
|
||||
});
|
||||
});
|
||||
|
||||
describe('editEPerson', () => {
|
||||
it('should dispatch a EDIT_EPERSON action with the EPerson to start editing', () => {
|
||||
service.editEPerson(EPersonMock);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new EPeopleRegistryEditEPersonAction(EPersonMock));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEPerson', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(EPersonMock));
|
||||
service.deleteEPerson(EPersonMock).subscribe();
|
||||
});
|
||||
|
||||
it('should send DeleteRequest', () => {
|
||||
const expected = new DeleteByIDRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, EPersonMock.uuid);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||
return observableOf(new RemoteData(false, false, true, undefined, obj));
|
||||
}
|
||||
|
||||
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||
diff(object1: Item, object2: Item): Operation[] {
|
||||
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||
}
|
||||
}
|
@@ -1,31 +1,52 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
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 { CoreState } from '../core.reducers';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { DataService } from '../data/data.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 { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
|
||||
import { EPerson } from './models/eperson.model';
|
||||
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()
|
||||
@dataService(EPERSON)
|
||||
export class EPersonDataService extends DataService<EPerson> {
|
||||
|
||||
protected linkPath: 'epersons';
|
||||
protected linkPath = 'epersons';
|
||||
protected searchByEmailPath = 'byEmail';
|
||||
protected searchByMetadataPath = 'byMetadata';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected store: Store<any>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
@@ -35,4 +56,167 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { IndexName } from './index.reducer';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { getUrlWithoutEmbedParams } from './index.selectors';
|
||||
|
||||
@Injectable()
|
||||
export class UUIDIndexEffects {
|
||||
@@ -47,7 +48,7 @@ export class UUIDIndexEffects {
|
||||
map((action: RequestConfigureAction) => {
|
||||
return new AddToIndexAction(
|
||||
IndexName.REQUEST,
|
||||
action.payload.href,
|
||||
getUrlWithoutEmbedParams(action.payload.href),
|
||||
action.payload.uuid
|
||||
);
|
||||
})
|
||||
|
32
src/app/core/index/index.selectors.spec.ts
Normal file
32
src/app/core/index/index.selectors.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getUrlWithoutEmbedParams } from './index.selectors';
|
||||
|
||||
describe(`index selectors`, () => {
|
||||
|
||||
describe(`getUrlWithoutEmbedParams`, () => {
|
||||
|
||||
it(`should return a url without its embed params`, () => {
|
||||
const source = 'https://rest.api/resource?a=1&embed=2&b=3&embed=4/5&c=6&embed=7/8/9';
|
||||
const result = getUrlWithoutEmbedParams(source);
|
||||
expect(result).toBe('https://rest.api/resource?a=1&b=3&c=6');
|
||||
});
|
||||
|
||||
it(`should return a url without embed params unmodified`, () => {
|
||||
const source = 'https://rest.api/resource?a=1&b=3&c=6';
|
||||
const result = getUrlWithoutEmbedParams(source);
|
||||
expect(result).toBe(source);
|
||||
});
|
||||
|
||||
it(`should return a string that isn't a url unmodified`, () => {
|
||||
const source = 'a=1&embed=2&b=3&embed=4/5&c=6&embed=7/8/9';
|
||||
const result = getUrlWithoutEmbedParams(source);
|
||||
expect(result).toBe(source);
|
||||
});
|
||||
|
||||
it(`should return undefined or null unmodified`, () => {
|
||||
expect(getUrlWithoutEmbedParams(undefined)).toBe(undefined);
|
||||
expect(getUrlWithoutEmbedParams(null)).toBe(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@@ -1,8 +1,41 @@
|
||||
import { createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { IndexName, IndexState, MetaIndexState } from './index.reducer';
|
||||
import * as parse from 'url-parse';
|
||||
|
||||
/**
|
||||
* Return the given url without `embed` params.
|
||||
*
|
||||
* E.g. https://rest.api/resource?size=5&embed=subresource&rpp=3
|
||||
* becomes https://rest.api/resource?size=5&rpp=3
|
||||
*
|
||||
* When you index a request url you don't want to include
|
||||
* embed params because embedded data isn't relevant when
|
||||
* you want to know
|
||||
*
|
||||
* @param url The url to use
|
||||
*/
|
||||
export const getUrlWithoutEmbedParams = (url: string): string => {
|
||||
if (isNotEmpty(url)) {
|
||||
const parsed = parse(url);
|
||||
if (isNotEmpty(parsed.query)) {
|
||||
const parts = parsed.query.split(/[?|&]/)
|
||||
.filter((part: string) => isNotEmpty(part))
|
||||
.filter((part: string) => !part.startsWith('embed='));
|
||||
let args = '';
|
||||
if (isNotEmpty(parts)) {
|
||||
args = `?${parts.join('&')}`;
|
||||
}
|
||||
url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the MetaIndexState based on the CoreSate
|
||||
@@ -74,7 +107,7 @@ export const selfLinkFromUuidSelector =
|
||||
export const uuidFromHrefSelector =
|
||||
(href: string): MemoizedSelector<CoreState, string> => createSelector(
|
||||
requestIndexSelector,
|
||||
(state: IndexState) => hasValue(state) ? state[href] : undefined
|
||||
(state: IndexState) => hasValue(state) ? state[getUrlWithoutEmbedParams(href)] : undefined
|
||||
);
|
||||
|
||||
/**
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
SetQueryParameterAction,
|
||||
SetQueryParametersAction
|
||||
} from './route.actions';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Interface to represent the parameter state of a current route in the store
|
||||
@@ -81,7 +82,8 @@ function addParameter(state: RouteState, action: AddParameterAction | AddQueryPa
|
||||
* @param paramType The type of parameter to set: route or query parameter
|
||||
*/
|
||||
function setParameters(state: RouteState, action: SetParametersAction | SetQueryParametersAction, paramType: string): RouteState {
|
||||
return Object.assign({}, state, { [paramType]: { [action.payload.key]: action.payload.value } });
|
||||
const param = isNotEmpty(action.payload) ? { [paramType]: { [action.payload.key]: action.payload.value } } : {};
|
||||
return Object.assign({}, state, param);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,7 +6,7 @@ import { combineLatest, Observable } from 'rxjs';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParametersAction } from './route.actions';
|
||||
import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParameterAction, SetQueryParametersAction } from './route.actions';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
@@ -196,6 +196,10 @@ export class RouteService {
|
||||
this.store.dispatch(new SetParameterAction(key, value));
|
||||
}
|
||||
|
||||
public setQueryParameter(key, value) {
|
||||
this.store.dispatch(new SetQueryParameterAction(key, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current route parameters and query parameters in the store
|
||||
*/
|
||||
|
@@ -10,4 +10,5 @@ export enum Context {
|
||||
Workspace = 'workspace',
|
||||
AdminMenu = 'adminMenu',
|
||||
SubmissionModal = 'submissionModal',
|
||||
AdminSearch = 'adminSearch',
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ import { Relationship } from './item-relationships/relationship.model';
|
||||
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
||||
import { ITEM } from './item.resource-type';
|
||||
import { ChildHALResource } from './child-hal-resource.model';
|
||||
import { Version } from './version.model';
|
||||
import { VERSION } from './version.resource-type';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Item
|
||||
@@ -67,6 +69,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
||||
bundles: HALLink;
|
||||
owningCollection: HALLink;
|
||||
templateItemOf: HALLink;
|
||||
version: HALLink;
|
||||
self: HALLink;
|
||||
};
|
||||
|
||||
@@ -77,6 +80,13 @@ export class Item extends DSpaceObject implements ChildHALResource {
|
||||
@link(COLLECTION)
|
||||
owningCollection?: Observable<RemoteData<Collection>>;
|
||||
|
||||
/**
|
||||
* The version this item represents in its history
|
||||
* Will be undefined unless the version {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(VERSION)
|
||||
version?: Observable<RemoteData<Version>>;
|
||||
|
||||
/**
|
||||
* The list of Bundles inside this Item
|
||||
* Will be undefined unless the bundles {@link HALLink} has been resolved.
|
||||
|
39
src/app/core/shared/version-history.model.ts
Normal file
39
src/app/core/shared/version-history.model.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { deserialize, autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { Version } from './version.model';
|
||||
import { VERSION_HISTORY } from './version-history.resource-type';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { VERSION } from './version.resource-type';
|
||||
|
||||
/**
|
||||
* Class representing a DSpace Version History
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class VersionHistory extends DSpaceObject {
|
||||
static type = VERSION_HISTORY;
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
versions: HALLink;
|
||||
};
|
||||
|
||||
/**
|
||||
* The identifier of this Version History
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The list of versions within this history
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@link(VERSION, true)
|
||||
versions: Observable<RemoteData<PaginatedList<Version>>>;
|
||||
}
|
9
src/app/core/shared/version-history.resource-type.ts
Normal file
9
src/app/core/shared/version-history.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for VersionHistory
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const VERSION_HISTORY = new ResourceType('versionhistory');
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user