mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-15 14:03:06 +00:00
Merge remote-tracking branch 'origin/main' into fix-versioning-button
This commit is contained in:
@@ -131,6 +131,10 @@ submission:
|
||||
# NOTE: after how many time (milliseconds) submission is saved automatically
|
||||
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
||||
timer: 0
|
||||
# Always show the duplicate detection section if enabled, even if there are no potential duplicates detected
|
||||
# (a message will be displayed to indicate no matches were found)
|
||||
duplicateDetection:
|
||||
alwaysShowSection: false
|
||||
icons:
|
||||
metadata:
|
||||
# NOTE: example of configuration
|
||||
@@ -427,9 +431,67 @@ comcolSelectionSort:
|
||||
|
||||
|
||||
# Search settings
|
||||
search:
|
||||
search:
|
||||
# Settings to enable/disable or configure advanced search filters.
|
||||
advancedFilters:
|
||||
enabled: false
|
||||
# List of filters to enable in "Advanced Search" dropdown
|
||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
||||
|
||||
|
||||
# Notify metrics
|
||||
# Configuration for Notify Admin Dashboard for metrics visualization
|
||||
notifyMetrics:
|
||||
# Configuration for received messages
|
||||
- title: 'admin-notify-dashboard.received-ldn'
|
||||
boxes:
|
||||
- color: '#B8DAFF'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.accepted'
|
||||
config: 'NOTIFY.incoming.accepted'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description'
|
||||
- color: '#D4EDDA'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.processed'
|
||||
config: 'NOTIFY.incoming.processed'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.failure'
|
||||
config: 'NOTIFY.incoming.failure'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted'
|
||||
config: 'NOTIFY.incoming.untrusted'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description'
|
||||
- color: '#43515F'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems'
|
||||
textColor: '#fff'
|
||||
config: 'NOTIFY.incoming.involvedItems'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description'
|
||||
# Configuration for outgoing messages
|
||||
- title: 'admin-notify-dashboard.generated-ldn'
|
||||
boxes:
|
||||
- color: '#B8DAFF'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued'
|
||||
config: 'NOTIFY.outgoing.queued'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description'
|
||||
- color: '#FDEEBB'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry'
|
||||
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.failure'
|
||||
config: 'NOTIFY.outgoing.failure'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description'
|
||||
- color: '#43515F'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems'
|
||||
textColor: '#fff'
|
||||
config: 'NOTIFY.outgoing.involvedItems'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description'
|
||||
- color: '#D4EDDA'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered'
|
||||
config: 'NOTIFY.outgoing.delivered'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -15,7 +15,10 @@ module.exports = function (config) {
|
||||
],
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
captureConsole: false
|
||||
captureConsole: false,
|
||||
jasmine: {
|
||||
failSpecWithNoExpectations: true
|
||||
}
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
||||
|
@@ -128,7 +128,7 @@
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.0",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"sanitize-html": "^2.12.1",
|
||||
"sortablejs": "1.15.0",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
|
@@ -5,8 +5,8 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } 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 { NgbModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
@@ -18,8 +18,6 @@ import { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
|
||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
@@ -31,17 +29,15 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
let fixture: ComponentFixture<EPeopleRegistryComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
let mockEPeople;
|
||||
let mockEPeople: EPerson[];
|
||||
let ePersonDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let modalService;
|
||||
let modalService: NgbModal;
|
||||
let paginationService: PaginationServiceStub;
|
||||
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(waitForAsync(async () => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
@@ -99,7 +95,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
});
|
||||
return observableOf(true);
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
@@ -119,17 +115,11 @@ describe('EPeopleRegistryComponent', () => {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent],
|
||||
providers: [
|
||||
@@ -148,7 +138,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
modalService = (component as any).modalService;
|
||||
modalService = TestBed.inject(NgbModal);
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -158,10 +148,10 @@ describe('EPeopleRegistryComponent', () => {
|
||||
});
|
||||
|
||||
it('should display list of ePeople', () => {
|
||||
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
const ePeopleIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
expect(ePeopleIdsFound.length).toEqual(2);
|
||||
mockEPeople.map((ePerson: EPerson) => {
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
expect(ePeopleIdsFound.find((foundEl: DebugElement) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
@@ -169,7 +159,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching with scope/query (scope metadata)', () => {
|
||||
let ePeopleIdsFound;
|
||||
let ePeopleIdsFound: DebugElement[];
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'metadata', query: EPersonMock2.name });
|
||||
tick();
|
||||
@@ -179,14 +169,14 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
expect(ePeopleIdsFound.find((foundEl: DebugElement) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching with scope/query (scope email)', () => {
|
||||
let ePeopleIdsFound;
|
||||
let ePeopleIdsFound: DebugElement[];
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'email', query: EPersonMock.email });
|
||||
tick();
|
||||
@@ -196,7 +186,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
expect(ePeopleIdsFound.find((foundEl: DebugElement) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
@@ -228,19 +218,12 @@ describe('EPeopleRegistryComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete EPerson button when the isAuthorized returns false', () => {
|
||||
let ePeopleDeleteButton;
|
||||
beforeEach(() => {
|
||||
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||
component.initialisePage();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be disabled', () => {
|
||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should hide delete EPerson button when the isAuthorized returns false', () => {
|
||||
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||
component.initialisePage();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('#epeople tr td div button.delete-button'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
@@ -102,8 +102,13 @@
|
||||
id="ldnUrl"
|
||||
name="ldnUrl"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" >
|
||||
<div *ngIf="formModel.get('ldnUrl').errors['required']" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
</div>
|
||||
<div *ngIf="formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -6,7 +6,12 @@ import {
|
||||
TemplateRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
|
||||
@@ -167,6 +172,9 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
this.closeModal();
|
||||
this.sendBack();
|
||||
} else {
|
||||
if (!this.formModel.errors) {
|
||||
this.setLdnUrlError();
|
||||
}
|
||||
this.notificationService.error(this.translateService.get('ldn-service-notification.created.failure.title'),
|
||||
this.translateService.get('ldn-service-notification.created.failure.body'));
|
||||
this.closeModal();
|
||||
@@ -405,6 +413,9 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.success.content'));
|
||||
} else {
|
||||
if (!this.formModel.errors) {
|
||||
this.setLdnUrlError();
|
||||
}
|
||||
this.notificationService.error(this.translateService.get('admin.registries.services-formats.modify.failure.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.failure.content'));
|
||||
this.closeModal();
|
||||
@@ -554,4 +565,14 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
automatic: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* set ldnUrl error in case of unprocessable entity and provided value
|
||||
*/
|
||||
private setLdnUrlError(): void {
|
||||
const control = this.formModel.controls.ldnUrl;
|
||||
const controlErrors = control.errors || {};
|
||||
control.setErrors({...controlErrors, ldnUrlAlreadyAssociated: true });
|
||||
}
|
||||
}
|
||||
|
@@ -78,6 +78,7 @@ describe('LdnServicesService test', () => {
|
||||
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$({}, 500),
|
||||
buildList: cold('a', { a: remoteDataMocks.Success })
|
||||
});
|
||||
|
||||
@@ -111,6 +112,20 @@ describe('LdnServicesService test', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke service', (done) => {
|
||||
const constraints = [{void: true}];
|
||||
const files = [new File([],'fileName')];
|
||||
spyOn(service as any, 'getInvocationFormData');
|
||||
spyOn(service, 'getBrowseEndpoint').and.returnValue(observableOf('testEndpoint'));
|
||||
service.invoke('serviceName', 'serviceId', constraints, files).subscribe(result => {
|
||||
expect((service as any).getInvocationFormData).toHaveBeenCalledWith(constraints, files);
|
||||
expect(service.getBrowseEndpoint).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(RemoteData);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {ChangeDetectorRef, EventEmitter} from '@angular/core';
|
||||
import { ChangeDetectorRef, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
@@ -21,8 +21,6 @@ describe('LdnServicesOverviewComponent', () => {
|
||||
let ldnServicesService;
|
||||
let paginationService;
|
||||
let modalService: NgbModal;
|
||||
let notificationsService: NotificationsService;
|
||||
let translateService: TranslateService;
|
||||
|
||||
const translateServiceStub = {
|
||||
get: () => of('translated-text'),
|
||||
@@ -33,7 +31,11 @@ describe('LdnServicesOverviewComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
paginationService = new PaginationServiceStub();
|
||||
ldnServicesService = jasmine.createSpyObj('LdnServicesService', ['findAll', 'delete', 'patch']);
|
||||
ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
|
||||
'findAll': createSuccessfulRemoteDataObject$({}),
|
||||
'delete': createSuccessfulRemoteDataObject$({}),
|
||||
'patch': createSuccessfulRemoteDataObject$({}),
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [LdnServicesOverviewComponent],
|
||||
@@ -50,9 +52,10 @@ describe('LdnServicesOverviewComponent', () => {
|
||||
}
|
||||
},
|
||||
{provide: ChangeDetectorRef, useValue: {}},
|
||||
{provide: NotificationsService, useValue: NotificationsServiceStub},
|
||||
{provide: NotificationsService, useValue: new NotificationsServiceStub()},
|
||||
{provide: TranslateService, useValue: translateServiceStub},
|
||||
]
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -62,8 +65,6 @@ describe('LdnServicesOverviewComponent', () => {
|
||||
ldnServicesService = TestBed.inject(LdnServicesService);
|
||||
paginationService = TestBed.inject(PaginationService);
|
||||
modalService = TestBed.inject(NgbModal);
|
||||
notificationsService = TestBed.inject(NotificationsService);
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
component.modalRef = jasmine.createSpyObj({close: null});
|
||||
component.isProcessingSub = jasmine.createSpyObj({unsubscribe: null});
|
||||
component.ldnServicesRD$ = of({} as RemoteData<PaginatedList<LdnService>>);
|
||||
@@ -141,4 +142,22 @@ describe('LdnServicesOverviewComponent', () => {
|
||||
expect(deleteSpy).toHaveBeenCalledWith(serviceId);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('selectServiceToDelete', () => {
|
||||
it('should set service to delete', fakeAsync(() => {
|
||||
spyOn(component, 'openDeleteModal');
|
||||
const serviceId = 123;
|
||||
component.selectServiceToDelete(serviceId);
|
||||
expect(component.selectedServiceId).toEqual(serviceId);
|
||||
expect(component.openDeleteModal).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('toggleStatus', () => {
|
||||
it('should toggle status', (() => {
|
||||
component.toggleStatus({enabled: false}, ldnServicesService);
|
||||
expect(ldnServicesService.patch).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -0,0 +1,64 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import {
|
||||
SiteAdministratorGuard
|
||||
} from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
AdminNotifyIncomingComponent
|
||||
} from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
||||
import {
|
||||
AdminNotifyOutgoingComponent
|
||||
} from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||
import { NotifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
|
||||
path: '',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
component: AdminNotifyDashboardComponent,
|
||||
pathMatch: 'full',
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inbound',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
component: AdminNotifyIncomingComponent,
|
||||
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'outbound',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
component: AdminNotifyOutgoingComponent,
|
||||
canActivate: [SiteAdministratorGuard, NotifyInfoGuard],
|
||||
data: {
|
||||
title: 'admin.notify.dashboard.page.title',
|
||||
breadcrumbKey: 'admin.notify.dashboard',
|
||||
},
|
||||
}
|
||||
])
|
||||
],
|
||||
})
|
||||
/**
|
||||
* Routing module for the Notifications section of the admin sidebar
|
||||
*/
|
||||
export class AdminNotifyDashboardRoutingModule {
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h1>
|
||||
<div class="my-4">{{'admin-notify-dashboard.description' | translate}}</div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'inbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
<div class="mt-2">
|
||||
<ds-admin-notify-metrics *ngIf="(notifyMetricsRows$ | async)?.length" [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SearchService } from '../../core/shared/search/search.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { AdminNotifySearchResult } from './models/admin-notify-message-search-result.model';
|
||||
import { AdminNotifyMessage } from './models/admin-notify-message.model';
|
||||
|
||||
describe('AdminNotifyDashboardComponent', () => {
|
||||
let component: AdminNotifyDashboardComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyDashboardComponent>;
|
||||
|
||||
let item1;
|
||||
let item2;
|
||||
let item3;
|
||||
let searchResult1;
|
||||
let searchResult2;
|
||||
let searchResult3;
|
||||
let results;
|
||||
|
||||
const mockBoxes = [
|
||||
{ title: 'admin-notify-dashboard.received-ldn', boxes: [ undefined, undefined, undefined, undefined, undefined ] },
|
||||
{ title: 'admin-notify-dashboard.generated-ldn', boxes: [ undefined, undefined, undefined, undefined, undefined ] }
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
item1 = Object.assign(new AdminNotifyMessage(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
|
||||
item2 = Object.assign(new AdminNotifyMessage(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
|
||||
item3 = Object.assign(new AdminNotifyMessage(), { uuid: 'c3bcbff5-ec0c-4831-8e4c-94b9c933ccac' });
|
||||
searchResult1 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item1 });
|
||||
searchResult2 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item2 });
|
||||
searchResult3 = Object.assign(new AdminNotifySearchResult(), { indexableObject: item3 });
|
||||
results = buildPaginatedList(undefined, [searchResult1, searchResult2, searchResult3]);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NgbNavModule],
|
||||
declarations: [ AdminNotifyDashboardComponent ],
|
||||
providers: [{ provide: SearchService, useValue: { search: () => createSuccessfulRemoteDataObject$(results)}}]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyDashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', (done) => {
|
||||
component.notifyMetricsRows$.subscribe(boxes => {
|
||||
expect(boxes).toEqual(mockBoxes);
|
||||
done();
|
||||
});
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,95 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SearchService } from '../../core/shared/search/search.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchObjects } from '../../shared/search/models/search-objects.model';
|
||||
import { AdminNotifyMetricsBox, AdminNotifyMetricsRow } from './admin-notify-metrics/admin-notify-metrics.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-dashboard',
|
||||
templateUrl: './admin-notify-dashboard.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* Component used for visual representation and search of LDN messages for Admins
|
||||
*/
|
||||
export class AdminNotifyDashboardComponent implements OnInit{
|
||||
|
||||
public notifyMetricsRows$: Observable<AdminNotifyMetricsRow[]>;
|
||||
|
||||
private metricsConfig = environment.notifyMetrics;
|
||||
private singleResultOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'single-result-options',
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
constructor(private searchService: SearchService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const mertricsRowsConfigurations = this.metricsConfig
|
||||
.map(row => row.boxes)
|
||||
.map(boxes => boxes.map(box => box.config).filter(config => !!config));
|
||||
const flatConfigurations = [].concat(...mertricsRowsConfigurations.map((config) => config));
|
||||
const searchConfigurations = flatConfigurations
|
||||
.map(config => Object.assign(new PaginatedSearchOptions({}),
|
||||
{ configuration: config, pagination: this.singleResultOptions }
|
||||
));
|
||||
|
||||
this.notifyMetricsRows$ = forkJoin(searchConfigurations.map(config => this.searchService.search(config)
|
||||
.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map(response => this.mapSearchObjectsToMetricsBox(response.payload)),
|
||||
)
|
||||
)
|
||||
).pipe(
|
||||
map(metricBoxes => this.mapUpdatedBoxesToMetricsRows(metricBoxes))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to map received SearchObjects to notify boxes config
|
||||
*
|
||||
* @param searchObject The object to map
|
||||
* @private
|
||||
*/
|
||||
private mapSearchObjectsToMetricsBox(searchObject: SearchObjects<DSpaceObject>): AdminNotifyMetricsBox {
|
||||
const count = searchObject.pageInfo.totalElements;
|
||||
const objectConfig = searchObject.configuration;
|
||||
const metricsBoxes = [].concat(...this.metricsConfig.map((config) => config.boxes));
|
||||
|
||||
return {
|
||||
...metricsBoxes.find(box => box.config === objectConfig),
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to map updated boxes with count to each row of the configuration
|
||||
*
|
||||
* @param boxesWithCount The object to map
|
||||
* @private
|
||||
*/
|
||||
private mapUpdatedBoxesToMetricsRows(boxesWithCount: AdminNotifyMetricsBox[]): AdminNotifyMetricsRow[] {
|
||||
return this.metricsConfig.map(row => {
|
||||
return {
|
||||
...row,
|
||||
boxes: row.boxes.map(rowBox => boxesWithCount.find(boxWithCount => boxWithCount.config === rowBox.config))
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||
import { AdminNotifyDashboardRoutingModule } from './admin-notify-dashboard-routing.module';
|
||||
import { AdminNotifyMetricsComponent } from './admin-notify-metrics/admin-notify-metrics.component';
|
||||
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { SearchModule } from '../../shared/search/search.module';
|
||||
import { SearchPageModule } from '../../search-page/search-page.module';
|
||||
import {
|
||||
AdminNotifyOutgoingComponent
|
||||
} from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import {
|
||||
AdminNotifySearchResultComponent
|
||||
} from './admin-notify-search-result/admin-notify-search-result.component';
|
||||
import { AdminNotifyMessagesService } from './services/admin-notify-messages.service';
|
||||
import { AdminNotifyLogsResultComponent } from './admin-notify-logs/admin-notify-logs-result/admin-notify-logs-result.component';
|
||||
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
AdminNotifySearchResultComponent
|
||||
];
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedModule,
|
||||
AdminNotifyDashboardRoutingModule,
|
||||
SearchModule,
|
||||
SearchPageModule
|
||||
],
|
||||
providers: [
|
||||
AdminNotifyMessagesService,
|
||||
DatePipe
|
||||
],
|
||||
declarations: [
|
||||
...ENTRY_COMPONENTS,
|
||||
AdminNotifyDashboardComponent,
|
||||
AdminNotifyMetricsComponent,
|
||||
AdminNotifyIncomingComponent,
|
||||
AdminNotifyOutgoingComponent,
|
||||
AdminNotifyDetailModalComponent,
|
||||
AdminNotifySearchResultComponent,
|
||||
AdminNotifyLogsResultComponent
|
||||
]
|
||||
})
|
||||
export class AdminNotifyDashboardModule {
|
||||
static withEntryComponents() {
|
||||
return {
|
||||
ngModule: AdminNotifyDashboardModule,
|
||||
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{'notify-message-modal.title' | translate}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div *ngFor="let key of notifyMessageKeys">
|
||||
<div class="row mb-4">
|
||||
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
|
||||
<div class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn-primary" (click)="toggleCoarMessage()">
|
||||
{{'notify-message-modal.show-message' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<pre @fadeIn [innerHTML]="notifyMessage.message" class="bg-secondary text-white mt-2 p-2" *ngIf="isCoarMessageVisible"></pre>
|
||||
</div>
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal.component';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AdminNotifyDetailModalComponent', () => {
|
||||
let component: AdminNotifyDetailModalComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyDetailModalComponent>;
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyDetailModalComponent ],
|
||||
providers: [{ provide: NgbActiveModal, useValue: modalStub }]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyDetailModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should close', () => {
|
||||
spyOn(component.response, 'emit');
|
||||
component.closeModal();
|
||||
expect(modalStub.close).toHaveBeenCalled();
|
||||
expect(component.response.emit).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
@@ -0,0 +1,49 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MissingTranslationHelper } from '../../../shared/translate/missing-translation.helper';
|
||||
import { fadeIn } from '../../../shared/animations/fade';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-detail-modal',
|
||||
templateUrl: './admin-notify-detail-modal.component.html',
|
||||
animations: [
|
||||
fadeIn
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Component for detailed view of LDN messages displayed in search result in AdminNotifyDashboardComponent
|
||||
*/
|
||||
|
||||
export class AdminNotifyDetailModalComponent {
|
||||
@Input() notifyMessage: AdminNotifyMessage;
|
||||
@Input() notifyMessageKeys: string[];
|
||||
|
||||
/**
|
||||
* An event fired when the modal is closed
|
||||
*/
|
||||
@Output()
|
||||
response = new EventEmitter<boolean>();
|
||||
|
||||
public isCoarMessageVisible = false;
|
||||
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal,
|
||||
public translationsService: TranslateService) {
|
||||
this.translationsService.missingTranslationHandler = new MissingTranslationHelper();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the modal and set the response to true so RootComponent knows the modal was closed
|
||||
*/
|
||||
closeModal() {
|
||||
this.activeModal.close();
|
||||
this.response.emit(true);
|
||||
}
|
||||
|
||||
toggleCoarMessage() {
|
||||
this.isCoarMessageVisible = !this.isCoarMessageVisible;
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h1>
|
||||
<div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../'">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../outbound'" [queryParams]="{view: 'table', configuration: 'NOTIFY.outgoing'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
|
||||
<ds-admin-notify-logs-result [defaultConfiguration]="'NOTIFY.incoming'" ></ds-admin-notify-logs-result>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,58 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyIncomingComponent } from './admin-notify-incoming.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../../shared/testing/route-service.stub';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { getMockRemoteDataBuildService } from '../../../../shared/mocks/remote-data-build.service.mock';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
describe('AdminNotifyIncomingComponent', () => {
|
||||
let component: AdminNotifyIncomingComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyIncomingComponent>;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getRootHref': '/api'
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': 'client/1234',
|
||||
'send': '',
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyIncomingComponent ],
|
||||
providers: [
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: SearchConfigurationService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyIncomingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,19 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-incoming',
|
||||
templateUrl: './admin-notify-incoming.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifyIncomingComponent {
|
||||
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-3 text-left h4">{{((isInbound$ | async) ? 'admin.notify.dashboard.inbound' : 'admin.notify.dashboard.outbound') | translate}}</div>
|
||||
<div class="col-md-9">
|
||||
<div class="h4">
|
||||
<button (click)="resetDefaultConfiguration()" *ngIf="(selectedSearchConfig$ | async) !== defaultConfiguration" class="badge badge-primary mr-1 mb-1">
|
||||
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
|
||||
<span> ×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-labels [inPlaceSearch]="true"></ds-search-labels>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<ds-themed-search
|
||||
[configuration]="selectedSearchConfig$ | async"
|
||||
[showViewModes]="false"
|
||||
[searchEnabled]="false"
|
||||
[context]="context"
|
||||
></ds-themed-search>
|
||||
</div>
|
||||
|
@@ -0,0 +1,49 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyLogsResultComponent } from './admin-notify-logs-result.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterStub } from '../../../../shared/testing/router.stub';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../../shared/testing/route-service.stub';
|
||||
|
||||
describe('AdminNotifyLogsResultComponent', () => {
|
||||
let component: AdminNotifyLogsResultComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyLogsResultComponent>;
|
||||
let objectCache: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let halService: HALEndpointService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyLogsResultComponent ],
|
||||
providers: [
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyLogsResultComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-logs-result',
|
||||
templateUrl: './admin-notify-logs-result.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* Component for visualization of search page and related results for the logs of the Notify dashboard
|
||||
*/
|
||||
|
||||
export class AdminNotifyLogsResultComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
defaultConfiguration: string;
|
||||
|
||||
|
||||
public selectedSearchConfig$: Observable<string>;
|
||||
public isInbound$: Observable<boolean>;
|
||||
|
||||
protected readonly context = Context.CoarNotify;
|
||||
|
||||
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
protected cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.selectedSearchConfig$ = this.searchConfigService.getCurrentConfiguration(this.defaultConfiguration);
|
||||
this.isInbound$ = this.selectedSearchConfig$.pipe(
|
||||
map(config => config.startsWith('NOTIFY.incoming'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset route state to default configuration
|
||||
*/
|
||||
public resetDefaultConfiguration() {
|
||||
//Idle navigation to trigger rendering of result on same page
|
||||
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
|
||||
this.router.navigate([this.getResolvedUrl(this.route.snapshot)], {
|
||||
queryParams: {
|
||||
configuration: this.defaultConfiguration,
|
||||
view: ViewMode.Table,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolved url from route
|
||||
*
|
||||
* @param route url path
|
||||
* @returns url path
|
||||
*/
|
||||
private getResolvedUrl(route: ActivatedRouteSnapshot): string {
|
||||
return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/');
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h1>
|
||||
<div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../'">{{'admin-notify-dashboard.metrics' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="'../inbound'" [queryParams]="{view: 'table', configuration: 'NOTIFY.incoming'}">{{'admin.notify.dashboard.inbound-logs' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||
</ul>
|
||||
|
||||
<ds-admin-notify-logs-result [defaultConfiguration]="'NOTIFY.outgoing'" ></ds-admin-notify-logs-result>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyOutgoingComponent } from './admin-notify-outgoing.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../../shared/testing/route-service.stub';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { getMockRemoteDataBuildService } from '../../../../shared/mocks/remote-data-build.service.mock';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
describe('AdminNotifyOutgoingComponent', () => {
|
||||
let component: AdminNotifyOutgoingComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyOutgoingComponent>;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': 'client/1234',
|
||||
'send': '',
|
||||
});
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getRootHref': '/api'
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyOutgoingComponent ],
|
||||
providers: [
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: SearchConfigurationService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyOutgoingComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,19 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-outgoing',
|
||||
templateUrl: './admin-notify-outgoing.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AdminNotifyOutgoingComponent {
|
||||
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
<div class="mb-5" *ngFor="let row of boxesConfig">
|
||||
<div class="mb-2">{{ row.title | translate }}</div>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-sm" *ngFor="let box of row.boxes">
|
||||
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,71 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyMetricsComponent } from './admin-notify-metrics.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
|
||||
describe('AdminNotifyMetricsComponent', () => {
|
||||
let component: AdminNotifyMetricsComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyMetricsComponent>;
|
||||
let router: RouterStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
router = Object.assign(new RouterStub(),
|
||||
{url : '/notify-dashboard'}
|
||||
);
|
||||
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifyMetricsComponent ],
|
||||
providers: [{provide: Router, useValue: router}]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyMetricsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should navigate to correct url based on config', () => {
|
||||
const searchConfig = 'test.involvedItems';
|
||||
const incomingConfig = 'NOTIFY.incoming.test';
|
||||
const outgoingConfig = 'NOTIFY.outgoing.test';
|
||||
const adminPath = '/admin/search';
|
||||
const routeExtras = {
|
||||
queryParams: {
|
||||
configuration: searchConfig,
|
||||
view: ViewMode.ListElement
|
||||
},
|
||||
};
|
||||
|
||||
const routeExtrasTable = {
|
||||
queryParams: {
|
||||
configuration: incomingConfig,
|
||||
view: ViewMode.Table
|
||||
},
|
||||
};
|
||||
|
||||
const routeExtrasTableOutgoing = {
|
||||
queryParams: {
|
||||
configuration: outgoingConfig,
|
||||
view: ViewMode.Table
|
||||
},
|
||||
};
|
||||
component.navigateToSelectedSearchConfig(searchConfig);
|
||||
expect(router.navigate).toHaveBeenCalledWith([adminPath], routeExtras);
|
||||
|
||||
component.navigateToSelectedSearchConfig(incomingConfig);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/notify-dashboard/inbound'], routeExtrasTable);
|
||||
|
||||
component.navigateToSelectedSearchConfig(outgoingConfig);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/notify-dashboard/outbound'], routeExtrasTableOutgoing);
|
||||
});
|
||||
});
|
@@ -0,0 +1,53 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-metrics',
|
||||
templateUrl: './admin-notify-metrics.component.html',
|
||||
})
|
||||
/**
|
||||
* Component used to display the number of notification for each configured box in the notifyMetrics section
|
||||
*/
|
||||
|
||||
export class AdminNotifyMetricsComponent {
|
||||
|
||||
@Input()
|
||||
boxesConfig: AdminNotifyMetricsRow[];
|
||||
|
||||
private incomingConfiguration = 'NOTIFY.incoming';
|
||||
private involvedItemsSuffix = 'involvedItems';
|
||||
private inboundPath = '/inbound';
|
||||
private outboundPath = '/outbound';
|
||||
private adminSearchPath = '/admin/search';
|
||||
|
||||
constructor(private router: Router) {
|
||||
}
|
||||
|
||||
|
||||
public navigateToSelectedSearchConfig(searchConfig: string) {
|
||||
const isRelatedItemsConfig = searchConfig.endsWith(this.involvedItemsSuffix);
|
||||
|
||||
if (isRelatedItemsConfig) {
|
||||
this.router.navigate([this.adminSearchPath], {
|
||||
queryParams: {
|
||||
configuration: searchConfig,
|
||||
view: ViewMode.ListElement
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isIncomingConfig = searchConfig.startsWith(this.incomingConfiguration);
|
||||
const selectedPath = isIncomingConfig ? this.inboundPath : this.outboundPath;
|
||||
|
||||
this.router.navigate([`${this.router.url}${selectedPath}`], {
|
||||
queryParams: {
|
||||
configuration: searchConfig,
|
||||
view: ViewMode.Table
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* The properties for each Box to be displayed in rows in the AdminNotifyMetricsComponent
|
||||
*/
|
||||
|
||||
export interface AdminNotifyMetricsBox {
|
||||
color: string;
|
||||
textColor?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
config: string;
|
||||
count?: number;
|
||||
}
|
||||
/**
|
||||
* The properties for each Row containing a list of AdminNotifyMetricsBox to be displayed in the AdminNotifyMetricsComponent
|
||||
*/
|
||||
export interface AdminNotifyMetricsRow {
|
||||
title: string;
|
||||
boxes: AdminNotifyMetricsBox[]
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr class="text-nowrap">
|
||||
<th scope="col">{{ 'notify-message-result.timestamp' | translate}}</th>
|
||||
<th scope="col">{{'notify-message-result.repositoryItem' | translate}}</th>
|
||||
<th scope="col">{{ 'notify-message-result.ldnService' | translate}}</th>
|
||||
<th scope="col">{{ 'notify-message-result.type' | translate }}</th>
|
||||
<th scope="col">{{ 'notify-message-result.status' | translate }}</th>
|
||||
<th scope="col">{{ 'notify-message-result.action' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let message of (messagesSubject$ | async)">
|
||||
<td class="text-nowrap">
|
||||
<div *ngIf="message.queueLastStartTime">{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
|
||||
<div *ngIf="!message.queueLastStartTime">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<ds-truncatable [id]="message.id">
|
||||
<ds-truncatable-part [id]="message.id" [minLines]="2">
|
||||
<a *ngIf="message.relatedItem" [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
|
||||
</ds-truncatable-part>
|
||||
</ds-truncatable>
|
||||
<div *ngIf="!message.relatedItem">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngIf="message.ldnService">{{ message.ldnService }}</div>
|
||||
<div *ngIf="!message.ldnService">n/a</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ message.activityStreamType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-nowrap">{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
|
||||
<button *ngIf="message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)"
|
||||
(click)="reprocessMessage(message)"
|
||||
class="btn btn-warning"
|
||||
>
|
||||
{{ 'notify-message-result.reprocess' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@@ -0,0 +1,182 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AdminNotifySearchResultComponent } from './admin-notify-search-result.component';
|
||||
import { AdminNotifyMessagesService } from '../services/admin-notify-messages.service';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf, of } from 'rxjs';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
|
||||
export const mockAdminNotifyMessages = [
|
||||
{
|
||||
'type': 'message',
|
||||
'id': 'urn:uuid:5fb3af44-d4f8-4226-9475-2d09c2d8d9e0',
|
||||
'coarNotifyType': 'coar-notify:ReviewAction',
|
||||
'activityStreamType': 'TentativeReject',
|
||||
'inReplyTo': 'urn:uuid:f7289ad5-0955-4c86-834c-fb54a736778b',
|
||||
'object': null,
|
||||
'context': '24d50450-9ff0-485f-82d4-fba1be42f3f9',
|
||||
'queueAttempts': 1,
|
||||
'queueLastStartTime': '2023-11-24T14:44:00.064+00:00',
|
||||
'origin': 12,
|
||||
'target': null,
|
||||
'queueStatusLabel': 'notify-queue-status.processed',
|
||||
'queueTimeout': '2023-11-24T15:44:00.064+00:00',
|
||||
'queueStatus': 3,
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'http://localhost:8080/server/api/ldn/messages/urn:uuid:5fb3af44-d4f8-4226-9475-2d09c2d8d9e0'
|
||||
}
|
||||
},
|
||||
'thumbnail': 'test',
|
||||
'item': {},
|
||||
'accessStatus': {},
|
||||
'ldnService': 'NOTIFY inbox - Automatic service',
|
||||
'relatedItem': 'test coar 2 demo',
|
||||
'message': '{"@context":["https://www.w3.org/ns/activitystreams","https://purl.org/coar/notify"],"id":"urn:uuid:668f26e0-2e8d-4118-b0d2-ee713523bc45","type":["Reject","coar-notify:IngestAction"],"actor":{"id":"https://generic-service.com","type":["Service"],"name":"Generic Service"},"context":{"id":"https://dspace-coar.4science.cloud/handle/123456789/28","type":["Document"],"ietf:cite-as":"https://doi.org/10.4598/12123488"},"object":{"id":"https://dspace-coar.4science.cloud/handle/123456789/28","type":["Offer"]},"origin":{"id":"https://generic-service.com/system","type":["Service"],"inbox":"https://notify-inbox.info/inbox7"},"target":{"id":"https://some-organisation.org","type":["Organization"],"inbox":"https://dspace-coar.4science.cloud/server/ldn/inbox"},"inReplyTo":"urn:uuid:d9b4010a-f128-4815-abb2-83707a2ee9cf"}'
|
||||
},
|
||||
{
|
||||
'type': 'message',
|
||||
'id': 'urn:uuid:544c8777-e826-4810-a625-3e394cc3660d',
|
||||
'coarNotifyType': 'coar-notify:IngestAction',
|
||||
'activityStreamType': 'Announce',
|
||||
'inReplyTo': 'urn:uuid:b2ad72d6-6ea9-464f-b385-29a78417f6b8',
|
||||
'object': null,
|
||||
'context': 'e657437a-0ee2-437d-916a-bba8c57bf40b',
|
||||
'queueAttempts': 1,
|
||||
'queueLastStartTime': null,
|
||||
'origin': 12,
|
||||
'target': null,
|
||||
'queueStatusLabel': 'notify-queue-status.unmapped_action',
|
||||
'queueTimeout': '2023-11-24T14:15:34.945+00:00',
|
||||
'queueStatus': 6,
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'http://localhost:8080/server/api/ldn/messages/urn:uuid:544c8777-e826-4810-a625-3e394cc3660d'
|
||||
}
|
||||
},
|
||||
'thumbnail': {},
|
||||
'item': {},
|
||||
'accessStatus': {},
|
||||
'ldnService': 'NOTIFY inbox - Automatic service',
|
||||
'relatedItem': 'test coar demo',
|
||||
'message': '{"@context":["https://www.w3.org/ns/activitystreams","https://purl.org/coar/notify"],"id":"urn:uuid:668f26e0-2e8d-4118-b0d2-ee713523bc45","type":["Reject","coar-notify:IngestAction"],"actor":{"id":"https://generic-service.com","type":["Service"],"name":"Generic Service"},"context":{"id":"https://dspace-coar.4science.cloud/handle/123456789/28","type":["Document"],"ietf:cite-as":"https://doi.org/10.4598/12123488"},"object":{"id":"https://dspace-coar.4science.cloud/handle/123456789/28","type":["Offer"]},"origin":{"id":"https://generic-service.com/system","type":["Service"],"inbox":"https://notify-inbox.info/inbox7"},"target":{"id":"https://some-organisation.org","type":["Organization"],"inbox":"https://dspace-coar.4science.cloud/server/ldn/inbox"},"inReplyTo":"urn:uuid:d9b4010a-f128-4815-abb2-83707a2ee9cf"}'
|
||||
}
|
||||
] as unknown as AdminNotifyMessage[];
|
||||
describe('AdminNotifySearchResultComponent', () => {
|
||||
let component: AdminNotifySearchResultComponent;
|
||||
let fixture: ComponentFixture<AdminNotifySearchResultComponent>;
|
||||
let objectCache: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let halService: HALEndpointService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let adminNotifyMessageService: AdminNotifyMessagesService;
|
||||
let searchConfigService: SearchConfigurationService;
|
||||
let modalService: NgbModal;
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const testObject = {
|
||||
uuid: 'test-property',
|
||||
name: 'test-property',
|
||||
values: ['value-1', 'value-2']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
beforeEach(async () => {
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: '' })
|
||||
});
|
||||
adminNotifyMessageService = jasmine.createSpyObj('adminNotifyMessageService', {
|
||||
getDetailedMessages: of(mockAdminNotifyMessages),
|
||||
reprocessMessage: of(mockAdminNotifyMessages),
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
searchConfigService = jasmine.createSpyObj('searchConfigService', {
|
||||
getCurrentConfiguration: of('NOTIFY.outgoing')
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ AdminNotifySearchResultComponent, AdminNotifyDetailModalComponent ],
|
||||
providers: [
|
||||
{ provide: AdminNotifyMessagesService, useValue: adminNotifyMessageService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: new RouterStub() },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigService },
|
||||
DatePipe
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifySearchResultComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.searchConfigService = searchConfigService;
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.isInbound).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should open modal', () => {
|
||||
component.openDetailModal(mockAdminNotifyMessages[0]);
|
||||
expect(modalService.open).toHaveBeenCalledWith(AdminNotifyDetailModalComponent);
|
||||
});
|
||||
|
||||
it('should map messages', (done) => {
|
||||
component.messagesSubject$.subscribe((messages) => {
|
||||
expect(messages).toEqual(mockAdminNotifyMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reprocess message', (done) => {
|
||||
component.reprocessMessage(mockAdminNotifyMessages[0]);
|
||||
component.messagesSubject$.subscribe((messages) => {
|
||||
expect(messages).toEqual(mockAdminNotifyMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe on destroy', () => {
|
||||
(component as any).subs = [of(null).subscribe()];
|
||||
|
||||
spyOn((component as any).subs[0], 'unsubscribe');
|
||||
component.ngOnDestroy();
|
||||
expect((component as any).subs[0].unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,157 @@
|
||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AdminNotifySearchResult } from '../models/admin-notify-message-search-result.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../core/shared/context.model';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import {
|
||||
tabulatableObjectsComponent
|
||||
} from '../../../shared/object-collection/shared/tabulatable-objects/tabulatable-objects.decorator';
|
||||
import {
|
||||
TabulatableResultListElementsComponent
|
||||
} from '../../../shared/object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { AdminNotifyMessagesService } from '../services/admin-notify-messages.service';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
@tabulatableObjectsComponent(PaginatedList<AdminNotifySearchResult>, ViewMode.Table, Context.CoarNotify)
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-search-result',
|
||||
templateUrl: './admin-notify-search-result.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Component for visualization in table format of the search results related to the AdminNotifyDashboardComponent
|
||||
*/
|
||||
|
||||
|
||||
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifySearchResult>, AdminNotifySearchResult> implements OnInit, OnDestroy{
|
||||
public messagesSubject$: BehaviorSubject<AdminNotifyMessage[]> = new BehaviorSubject([]);
|
||||
public reprocessStatus = 'QUEUE_STATUS_QUEUED_FOR_RETRY';
|
||||
//we check on one type of config to render specific table headers
|
||||
public isInbound: boolean;
|
||||
|
||||
/**
|
||||
* Statuses for which we display the reprocess button
|
||||
*/
|
||||
public validStatusesForReprocess = [
|
||||
'QUEUE_STATUS_UNTRUSTED',
|
||||
'QUEUE_STATUS_UNTRUSTED_IP',
|
||||
'QUEUE_STATUS_FAILED',
|
||||
'QUEUE_STATUS_UNMAPPED_ACTION'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Keys to be formatted as date
|
||||
* @private
|
||||
*/
|
||||
|
||||
private dateTypeKeys: string[] = ['queueLastStartTime', 'queueTimeout'];
|
||||
|
||||
/**
|
||||
* Keys to be not shown in detail
|
||||
* @private
|
||||
*/
|
||||
private messageKeys: string[] = [
|
||||
'type',
|
||||
'id',
|
||||
'coarNotifyType',
|
||||
'activityStreamType',
|
||||
'inReplyTo',
|
||||
'queueAttempts',
|
||||
'queueLastStartTime',
|
||||
'queueStatusLabel',
|
||||
'queueTimeout'
|
||||
];
|
||||
|
||||
/**
|
||||
* The format for the date values
|
||||
* @private
|
||||
*/
|
||||
private dateFormat = 'YYYY/MM/d hh:mm:ss';
|
||||
|
||||
constructor(private modalService: NgbModal,
|
||||
private adminNotifyMessagesService: AdminNotifyMessagesService,
|
||||
private datePipe: DatePipe,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map messages on init for readable representation
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.mapDetailsToMessages();
|
||||
this.subs.push(this.searchConfigService.getCurrentConfiguration('')
|
||||
.subscribe(configuration => {
|
||||
this.isInbound = configuration.startsWith('NOTIFY.incoming');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal for details visualization
|
||||
* @param notifyMessage the message to be displayed
|
||||
*/
|
||||
openDetailModal(notifyMessage: AdminNotifyMessage) {
|
||||
const modalRef = this.modalService.open(AdminNotifyDetailModalComponent);
|
||||
const messageToOpen = {...notifyMessage};
|
||||
|
||||
this.messageKeys.forEach(key => {
|
||||
if (this.dateTypeKeys.includes(key)) {
|
||||
messageToOpen[key] = this.datePipe.transform(messageToOpen[key], this.dateFormat);
|
||||
}
|
||||
});
|
||||
// format COAR message for technical visualization
|
||||
messageToOpen.message = JSON.stringify(JSON.parse(notifyMessage.message), null, 2);
|
||||
|
||||
modalRef.componentInstance.notifyMessage = messageToOpen;
|
||||
modalRef.componentInstance.notifyMessageKeys = this.messageKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprocess message in status QUEUE_STATUS_QUEUED_FOR_RETRY and update results
|
||||
* @param message the message to be reprocessed
|
||||
*/
|
||||
reprocessMessage(message: AdminNotifyMessage) {
|
||||
this.subs.push(
|
||||
this.adminNotifyMessagesService.reprocessMessage(message, this.messagesSubject$)
|
||||
.subscribe(response => {
|
||||
this.messagesSubject$.next(response);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map readable results to messages
|
||||
* @private
|
||||
*/
|
||||
private mapDetailsToMessages() {
|
||||
this.subs.push(this.adminNotifyMessagesService.getDetailedMessages(this.objects?.page.map(pageResult => pageResult.indexableObject))
|
||||
.subscribe(response => {
|
||||
this.messagesSubject$.next(response);
|
||||
}));
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
import { AdminNotifyMessage } from './admin-notify-message.model';
|
||||
import { searchResultFor } from '../../../shared/search/search-result-element-decorator';
|
||||
import { SearchResult } from '../../../shared/search/models/search-result.model';
|
||||
|
||||
|
||||
@searchResultFor(AdminNotifyMessage)
|
||||
export class AdminNotifySearchResult extends SearchResult<AdminNotifyMessage> {
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
import { typedObject } from '../../../core/cache/builders/build-decorators';
|
||||
import { ADMIN_NOTIFY_MESSAGE } from './admin-notify-message.resource-type';
|
||||
import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
import { Observable } from 'rxjs';
|
||||
/**
|
||||
* A message that includes admin notify info
|
||||
*/
|
||||
@typedObject
|
||||
@inheritSerialization(DSpaceObject)
|
||||
export class AdminNotifyMessage extends DSpaceObject {
|
||||
static type = ADMIN_NOTIFY_MESSAGE;
|
||||
|
||||
/**
|
||||
* The type of the resource
|
||||
*/
|
||||
@excludeFromEquals
|
||||
type = ADMIN_NOTIFY_MESSAGE;
|
||||
|
||||
/**
|
||||
* The id of the message
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The id of the notification
|
||||
*/
|
||||
@autoserialize
|
||||
notificationId: string;
|
||||
|
||||
/**
|
||||
* The type of the notification
|
||||
*/
|
||||
@autoserialize
|
||||
notificationType: string;
|
||||
|
||||
/**
|
||||
* The type of the notification
|
||||
*/
|
||||
@autoserialize
|
||||
coarNotifyType: string;
|
||||
|
||||
/**
|
||||
* The type of the activity
|
||||
*/
|
||||
@autoserialize
|
||||
activityStreamType: string;
|
||||
|
||||
/**
|
||||
* The object the message reply to
|
||||
*/
|
||||
@autoserialize
|
||||
inReplyTo: string;
|
||||
|
||||
/**
|
||||
* The object the message relates to
|
||||
*/
|
||||
@autoserialize
|
||||
object: string;
|
||||
|
||||
/**
|
||||
* The name of the related item
|
||||
*/
|
||||
@autoserialize
|
||||
relatedItem: string;
|
||||
|
||||
/**
|
||||
* The name of the related ldn service
|
||||
*/
|
||||
@autoserialize
|
||||
ldnService: string;
|
||||
|
||||
/**
|
||||
* The context of the message
|
||||
*/
|
||||
@autoserialize
|
||||
context: string;
|
||||
|
||||
/**
|
||||
* The related COAR message
|
||||
*/
|
||||
@autoserialize
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The attempts of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueAttempts: number;
|
||||
|
||||
/**
|
||||
* Timestamp of the last queue attempt
|
||||
*/
|
||||
@autoserialize
|
||||
queueLastStartTime: string;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
*/
|
||||
@autoserialize
|
||||
origin: number | string;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
*/
|
||||
@autoserialize
|
||||
target: number | string;
|
||||
|
||||
/**
|
||||
* The label for the status of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueStatusLabel: string;
|
||||
|
||||
/**
|
||||
* The timeout of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueTimeout: string;
|
||||
|
||||
/**
|
||||
* The status of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueStatus: number;
|
||||
|
||||
/**
|
||||
* Thumbnail link used when browsing items with showThumbs config enabled.
|
||||
*/
|
||||
@autoserialize
|
||||
thumbnail: string;
|
||||
|
||||
/**
|
||||
* The observable pointing to the item itself
|
||||
*/
|
||||
@autoserialize
|
||||
item: Observable<AdminNotifyMessage>;
|
||||
|
||||
/**
|
||||
* The observable pointing to the access status of the item
|
||||
*/
|
||||
@autoserialize
|
||||
accessStatus: Observable<AdminNotifyMessage>;
|
||||
|
||||
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: {
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
get self(): string {
|
||||
return this._links.self.href;
|
||||
}
|
||||
|
||||
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
|
||||
return [this.constructor as GenericConstructor<ListableObject>];
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from '../../../core/shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for AdminNotifyMessage
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ADMIN_NOTIFY_MESSAGE = new ResourceType('message');
|
@@ -0,0 +1,114 @@
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { AdminNotifyMessagesService } from './admin-notify-messages.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { LdnServicesService } from '../../admin-ldn-services/ldn-services-data/ldn-services-data.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { RequestEntry } from '../../../core/data/request-entry.model';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestEntryState } from '../../../core/data/request-entry-state.model';
|
||||
import {
|
||||
mockAdminNotifyMessages} from '../admin-notify-search-result/admin-notify-search-result.component.spec';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { deepClone } from 'fast-json-patch';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
|
||||
describe('AdminNotifyMessagesService test', () => {
|
||||
let service: AdminNotifyMessagesService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let notificationsService: NotificationsService;
|
||||
let ldnServicesService: LdnServicesService;
|
||||
let itemDataService: ItemDataService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
let mockMessages: AdminNotifyMessage[];
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/ldn/messages`;
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
const remoteDataMocks = {
|
||||
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||
};
|
||||
const testLdnServiceName = 'testLdnService';
|
||||
const testRelatedItemName = 'testRelatedItem';
|
||||
|
||||
function initTestService() {
|
||||
return new AdminNotifyMessagesService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
ldnServicesService,
|
||||
itemDataService
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockMessages = deepClone(mockAdminNotifyMessages);
|
||||
objectCache = {} as ObjectCacheService;
|
||||
notificationsService = {} as NotificationsService;
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: endpointURL } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: of(responseCacheEntry),
|
||||
getByUUID: of(responseCacheEntry),
|
||||
});
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: of(endpointURL)
|
||||
});
|
||||
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||
buildList: cold('a', { a: remoteDataMocks.Success }),
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$(mockMessages)
|
||||
});
|
||||
|
||||
ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
|
||||
findById: createSuccessfulRemoteDataObject$({name: testLdnServiceName}),
|
||||
});
|
||||
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$({name: testRelatedItemName}),
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
describe('Admin Notify service', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get details for messages', (done) => {
|
||||
service.getDetailedMessages(mockMessages).pipe(take(1)).subscribe((detailedMessages) => {
|
||||
expect(detailedMessages[0].ldnService).toEqual(testLdnServiceName);
|
||||
expect(detailedMessages[0].relatedItem).toEqual(testRelatedItemName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reprocess message', (done) => {
|
||||
const behaviorSubject = new BehaviorSubject(mockMessages);
|
||||
service.reprocessMessage(mockMessages[0], behaviorSubject).pipe(take(1)).subscribe((reprocessedMessages) => {
|
||||
expect(reprocessedMessages.length).toEqual(2);
|
||||
expect(reprocessedMessages).toEqual(mockMessages);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,100 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {dataService} from '../../../core/data/base/data-service.decorator';
|
||||
import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
|
||||
import {RequestService} from '../../../core/data/request.service';
|
||||
import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
|
||||
import {ObjectCacheService} from '../../../core/cache/object-cache.service';
|
||||
import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import { BehaviorSubject, from, Observable, of, scan } from 'rxjs';
|
||||
import { ADMIN_NOTIFY_MESSAGE } from '../models/admin-notify-message.resource-type';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { LdnServicesService } from '../../admin-ldn-services/ldn-services-data/ldn-services-data.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { PostRequest } from '../../../core/data/request.models';
|
||||
import { RestRequest } from '../../../core/data/rest-request.model';
|
||||
|
||||
/**
|
||||
* Injectable service responsible for fetching/sending data from/to the REST API on the messages endpoint.
|
||||
*
|
||||
* @export
|
||||
* @class AdminNotifyMessagesService
|
||||
* @extends {IdentifiableDataService<AdminNotifyMessage>}
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(ADMIN_NOTIFY_MESSAGE)
|
||||
export class AdminNotifyMessagesService extends IdentifiableDataService<AdminNotifyMessage> {
|
||||
|
||||
protected reprocessEndpoint = 'enqueueretry';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
private ldnServicesService: LdnServicesService,
|
||||
private itemDataService: ItemDataService,
|
||||
) {
|
||||
super('messages', requestService, rdbService, objectCache, halService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add detailed information to each message
|
||||
* @param messages the messages to which add detailded info
|
||||
*/
|
||||
public getDetailedMessages(messages: AdminNotifyMessage[]): Observable<AdminNotifyMessage[]> {
|
||||
return from(messages).pipe(
|
||||
mergeMap(message =>
|
||||
message.target || message.origin ? this.ldnServicesService.findById((message.target || message.origin).toString()).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map(detail => ({...message, ldnService: detail.name}))
|
||||
) : of(message),
|
||||
),
|
||||
mergeMap(message =>
|
||||
message.object || message.context ? this.itemDataService.findById(message.object || message.context).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map(detail => ({...message, relatedItem: detail.name}))
|
||||
) : of(message),
|
||||
),
|
||||
scan((acc: any, value: any) => [...acc, value], []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprocess message in status QUEUE_STATUS_QUEUED_FOR_RETRY and update results
|
||||
* @param message the message to reprocess
|
||||
* @param messageSubject the current visualised messages source
|
||||
*/
|
||||
public reprocessMessage(message: AdminNotifyMessage, messageSubject: BehaviorSubject<AdminNotifyMessage[]>): Observable<AdminNotifyMessage[]> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
return this.halService.getEndpoint(this.reprocessEndpoint).pipe(
|
||||
map(endpoint => endpoint.replace('{id}', message.id)),
|
||||
map((endpointURL: string) => new PostRequest(requestId, endpointURL)),
|
||||
tap(request => this.requestService.send(request)),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<AdminNotifyMessage>(request.uuid)),
|
||||
getFirstCompletedRemoteData(),
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
mergeMap(reprocessedMessage => this.getDetailedMessages([reprocessedMessage])),
|
||||
).pipe(
|
||||
mergeMap((newMessages) => messageSubject.pipe(
|
||||
map(messages => {
|
||||
const detailedReprocessedMessage = newMessages[0];
|
||||
const messageToUpdate = messages.find(currentMessage => currentMessage.id === message.id);
|
||||
const indexOfMessageToUpdate = messages.indexOf(messageToUpdate);
|
||||
detailedReprocessedMessage.target = message.target;
|
||||
detailedReprocessedMessage.object = message.object;
|
||||
detailedReprocessedMessage.origin = message.origin;
|
||||
detailedReprocessedMessage.context = message.context;
|
||||
messages[indexOfMessageToUpdate] = detailedReprocessedMessage;
|
||||
|
||||
return messages;
|
||||
})
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ export const REGISTRIES_MODULE_PATH = 'registries';
|
||||
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
|
||||
export const LDN_PATH = 'ldn';
|
||||
export const REPORTS_MODULE_PATH = 'reports';
|
||||
|
||||
export const NOTIFY_DASHBOARD_MODULE_PATH = 'notify-dashboard';
|
||||
|
||||
|
||||
export function getRegistriesModuleRoute() {
|
||||
|
@@ -8,7 +8,7 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
import {
|
||||
LDN_PATH,
|
||||
NOTIFICATIONS_MODULE_PATH,
|
||||
NOTIFICATIONS_MODULE_PATH, NOTIFY_DASHBOARD_MODULE_PATH,
|
||||
REGISTRIES_MODULE_PATH, REPORTS_MODULE_PATH,
|
||||
} from './admin-routing-paths';
|
||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||
@@ -88,6 +88,11 @@ import {
|
||||
loadChildren: () => import('./admin-reports/admin-reports.module')
|
||||
.then((m) => m.AdminReportsModule),
|
||||
},
|
||||
{
|
||||
path: NOTIFY_DASHBOARD_MODULE_PATH,
|
||||
loadChildren: () => import('./admin-notify-dashboard/admin-notify-dashboard.module')
|
||||
.then((m) => m.AdminNotifyDashboardModule),
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -1,14 +1,44 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CollectionAccessControlComponent } from './collection-access-control.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of, of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
|
||||
xdescribe('CollectionAccessControlComponent', () => {
|
||||
describe('CollectionAccessControlComponent', () => {
|
||||
let component: CollectionAccessControlComponent;
|
||||
let fixture: ComponentFixture<CollectionAccessControlComponent>;
|
||||
const testCommunity = Object.assign(new Community(),
|
||||
{
|
||||
type: 'community',
|
||||
metadata: {
|
||||
'dc.title': [{ value: 'community' }]
|
||||
},
|
||||
uuid: 'communityUUID',
|
||||
parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })),
|
||||
|
||||
_links: {
|
||||
parentCommunity: 'site',
|
||||
self: '/' + 'communityUUID'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const routeStub = {
|
||||
parent: {
|
||||
parent: {
|
||||
data: of({
|
||||
dso: createSuccessfulRemoteDataObject(testCommunity)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CollectionAccessControlComponent ]
|
||||
declarations: [ CollectionAccessControlComponent ],
|
||||
providers: [{ provide: ActivatedRoute, useValue: routeStub }]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
@@ -17,9 +47,17 @@ xdescribe('CollectionAccessControlComponent', () => {
|
||||
fixture = TestBed.createComponent(CollectionAccessControlComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set itemRD$', (done) => {
|
||||
component.itemRD$.subscribe(result => {
|
||||
expect(result).toEqual(createSuccessfulRemoteDataObject(testCommunity));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,25 +1,66 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommunityAccessControlComponent } from './community-access-control.component';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { of, of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
|
||||
xdescribe('CommunityAccessControlComponent', () => {
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
describe('CommunityAccessControlComponent', () => {
|
||||
let component: CommunityAccessControlComponent;
|
||||
let fixture: ComponentFixture<CommunityAccessControlComponent>;
|
||||
const testCommunity = Object.assign(new Community(),
|
||||
{
|
||||
type: 'community',
|
||||
metadata: {
|
||||
'dc.title': [{ value: 'community' }]
|
||||
},
|
||||
uuid: 'communityUUID',
|
||||
parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })),
|
||||
|
||||
_links: {
|
||||
parentCommunity: 'site',
|
||||
self: '/' + 'communityUUID'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const routeStub = {
|
||||
parent: {
|
||||
parent: {
|
||||
data: of({
|
||||
dso: createSuccessfulRemoteDataObject(testCommunity)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ CommunityAccessControlComponent ]
|
||||
declarations: [ CommunityAccessControlComponent ],
|
||||
providers: [{ provide: ActivatedRoute, useValue: routeStub }]
|
||||
})
|
||||
.compileComponents();
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommunityAccessControlComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set itemRD$', (done) => {
|
||||
component.itemRD$.subscribe(result => {
|
||||
expect(result).toEqual(createSuccessfulRemoteDataObject(testCommunity));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -30,7 +30,7 @@ import { ConfigurationProperty } from '../../../../core/shared/configuration-pro
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
|
||||
|
||||
describe('CommunityPageSubCollectionList Component', () => {
|
||||
describe('CommunityPageSubCollectionListComponent', () => {
|
||||
let comp: CommunityPageSubCollectionListComponent;
|
||||
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
|
||||
let collectionDataServiceStub: any;
|
||||
@@ -177,19 +177,19 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should display a list of collections', () => {
|
||||
waitForAsync(() => {
|
||||
subCollList = collections;
|
||||
fixture.detectChanges();
|
||||
it('should display a list of collections', async () => {
|
||||
subCollList = collections;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(collList.length).toEqual(5);
|
||||
expect(collList[0].nativeElement.textContent).toContain('Collection 1');
|
||||
expect(collList[1].nativeElement.textContent).toContain('Collection 2');
|
||||
expect(collList[2].nativeElement.textContent).toContain('Collection 3');
|
||||
expect(collList[3].nativeElement.textContent).toContain('Collection 4');
|
||||
expect(collList[4].nativeElement.textContent).toContain('Collection 5');
|
||||
});
|
||||
const collList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li'));
|
||||
expect(collList.length).toEqual(5);
|
||||
expect(collList[0].nativeElement.textContent).toContain('Collection 1');
|
||||
expect(collList[1].nativeElement.textContent).toContain('Collection 2');
|
||||
expect(collList[2].nativeElement.textContent).toContain('Collection 3');
|
||||
expect(collList[3].nativeElement.textContent).toContain('Collection 4');
|
||||
expect(collList[4].nativeElement.textContent).toContain('Collection 5');
|
||||
});
|
||||
|
||||
it('should not display the header when list of collections is empty', () => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -30,7 +30,7 @@ import { SearchConfigurationServiceStub } from '../../../../shared/testing/searc
|
||||
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
|
||||
describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
describe('CommunityPageSubCommunityListComponent', () => {
|
||||
let comp: CommunityPageSubCommunityListComponent;
|
||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||
let communityDataServiceStub: any;
|
||||
@@ -179,19 +179,19 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should display a list of sub-communities', () => {
|
||||
waitForAsync(() => {
|
||||
subCommList = subcommunities;
|
||||
fixture.detectChanges();
|
||||
it('should display a list of sub-communities', async () => {
|
||||
subCommList = subcommunities;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(subComList.length).toEqual(5);
|
||||
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
||||
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
||||
expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3');
|
||||
expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4');
|
||||
expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5');
|
||||
});
|
||||
const subComList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li'));
|
||||
expect(subComList.length).toEqual(5);
|
||||
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
||||
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
||||
expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3');
|
||||
expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4');
|
||||
expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5');
|
||||
});
|
||||
|
||||
it('should not display the header when list of sub-communities is empty', () => {
|
||||
|
@@ -20,9 +20,7 @@ describe(`AuthInterceptor`, () => {
|
||||
|
||||
const authServiceStub = new AuthServiceStub();
|
||||
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
dispatch: {},
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
select: observableOf(true)
|
||||
});
|
||||
|
||||
@@ -46,6 +44,10 @@ describe(`AuthInterceptor`, () => {
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('when has a valid token', () => {
|
||||
|
||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => {
|
||||
@@ -95,14 +97,11 @@ describe(`AuthInterceptor`, () => {
|
||||
});
|
||||
|
||||
it('should redirect to login', () => {
|
||||
|
||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
||||
|
||||
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
||||
// HttpTestingController.expectNone will throw an error when a requests is made
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
|
16
src/app/core/cache/object-cache.reducer.spec.ts
vendored
16
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -126,6 +126,10 @@ describe('objectCacheReducer', () => {
|
||||
deepFreeze(state);
|
||||
|
||||
objectCacheReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should remove the specified object from the cache in response to the REMOVE action', () => {
|
||||
@@ -149,6 +153,10 @@ describe('objectCacheReducer', () => {
|
||||
const action = new RemoveFromObjectCacheAction(selfLink1);
|
||||
// testState has already been frozen above
|
||||
objectCacheReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action', () => {
|
||||
@@ -164,6 +172,10 @@ describe('objectCacheReducer', () => {
|
||||
const action = new ResetObjectCacheTimestampsAction(new Date().getTime());
|
||||
// testState has already been frozen above
|
||||
objectCacheReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the ADD_PATCH action without affecting the previous state', () => {
|
||||
@@ -174,6 +186,10 @@ describe('objectCacheReducer', () => {
|
||||
}]);
|
||||
// testState has already been frozen above
|
||||
objectCacheReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should when the ADD_PATCH action dispatched', () => {
|
||||
|
@@ -52,12 +52,20 @@ describe('serverSyncBufferReducer', () => {
|
||||
const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST);
|
||||
// testState has already been frozen above
|
||||
serverSyncBufferReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the EMPTY action without affecting the previous state', () => {
|
||||
const action = new EmptySSBAction();
|
||||
// testState has already been frozen above
|
||||
serverSyncBufferReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NotifyInfoService } from './notify-info.service';
|
||||
import { ConfigurationDataService } from '../../data/configuration-data.service';
|
||||
import { of } from 'rxjs';
|
||||
import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
|
||||
describe('NotifyInfoService', () => {
|
||||
let service: NotifyInfoService;
|
||||
@@ -32,21 +32,21 @@ describe('NotifyInfoService', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should retrieve and map coar configuration', () => {
|
||||
const mockResponse = { payload: { values: ['true'] } };
|
||||
(configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse));
|
||||
it('should retrieve and map coar configuration', (done: DoneFn) => {
|
||||
(configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['true'] }));
|
||||
|
||||
service.isCoarConfigEnabled().subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve and map LDN local inbox URLs', () => {
|
||||
const mockResponse = { values: ['inbox1', 'inbox2'] };
|
||||
(configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse));
|
||||
it('should retrieve and map LDN local inbox URLs', (done: DoneFn) => {
|
||||
(configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['inbox1', 'inbox2'] }));
|
||||
|
||||
service.getCoarLdnLocalInboxUrls().subscribe((result) => {
|
||||
expect(result).toEqual(['inbox1', 'inbox2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -198,6 +198,8 @@ import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status
|
||||
import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
|
||||
import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters';
|
||||
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
||||
import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
|
||||
import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -234,6 +236,7 @@ const PROVIDERS = [
|
||||
HALEndpointService,
|
||||
HostWindowService,
|
||||
ItemDataService,
|
||||
SubmissionDuplicateDataService,
|
||||
MetadataService,
|
||||
ObjectCacheService,
|
||||
PaginationComponentOptions,
|
||||
@@ -411,6 +414,7 @@ export const models =
|
||||
Itemfilter,
|
||||
SubmissionCoarNotifyConfig,
|
||||
NotifyRequestsStatus,
|
||||
AdminNotifyMessage
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -96,12 +96,6 @@ describe('ExternalSourceService', () => {
|
||||
result.pipe(take(1)).subscribe();
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), false);
|
||||
});
|
||||
|
||||
it('should return the entries', () => {
|
||||
result.subscribe((resultRD) => {
|
||||
expect(resultRD.payload.page).toBe(entries);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -0,0 +1,81 @@
|
||||
import { NotifyRequestsStatusDataService } from './notify-services-status-data.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestEntry } from './request-entry.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestEntryState } from './request-entry-state.model';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { of } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model';
|
||||
|
||||
describe('NotifyRequestsStatusDataService test', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: NotifyRequestsStatusDataService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/suggestiontargets`;
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
const remoteDataMocks = {
|
||||
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||
};
|
||||
|
||||
function initTestService() {
|
||||
return new NotifyRequestsStatusDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
objectCache = {} as ObjectCacheService;
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: of(responseCacheEntry),
|
||||
getByUUID: of(responseCacheEntry),
|
||||
});
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: of(endpointURL)
|
||||
});
|
||||
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||
buildList: cold('a', { a: remoteDataMocks.Success }),
|
||||
buildFromHref: createSuccessfulRemoteDataObject$({test: 'test'})
|
||||
});
|
||||
|
||||
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
describe('getNotifyRequestsStatus', () => {
|
||||
it('should get notify status', (done) => {
|
||||
service.getNotifyRequestsStatus(requestUUID).subscribe((status) => {
|
||||
expect(halService.getEndpoint).toHaveBeenCalled();
|
||||
expect(requestService.generateRequestId).toHaveBeenCalled();
|
||||
expect(status).toEqual(createSuccessfulRemoteDataObject({test: 'test'} as unknown as NotifyRequestsStatus));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -21,7 +21,6 @@ export class NotifyRequestsStatusDataService extends IdentifiableDataService<Not
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected rdb: RemoteDataBuildService,
|
||||
) {
|
||||
super('notifyrequests', requestService, rdbService, objectCache, halService);
|
||||
}
|
||||
@@ -41,6 +40,6 @@ export class NotifyRequestsStatusDataService extends IdentifiableDataService<Not
|
||||
this.requestService.send(request, true);
|
||||
});
|
||||
|
||||
return this.rdb.buildFromHref(href$);
|
||||
return this.rdbService.buildFromHref(href$);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
@@ -11,13 +12,10 @@ import {
|
||||
RemoveFieldUpdateAction,
|
||||
RemoveObjectUpdatesAction
|
||||
} from './object-updates.actions';
|
||||
import {
|
||||
INotification,
|
||||
Notification
|
||||
} from '../../../shared/notifications/models/notification.model';
|
||||
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NoOpAction } from '../../../shared/ngrx/no-op.action';
|
||||
|
||||
describe('ObjectUpdatesEffects', () => {
|
||||
@@ -31,13 +29,7 @@ describe('ObjectUpdatesEffects', () => {
|
||||
providers: [
|
||||
ObjectUpdatesEffects,
|
||||
provideMockActions(() => actions),
|
||||
{
|
||||
provide: NotificationsService,
|
||||
useValue: {
|
||||
remove: (notification) => { /* empty */
|
||||
}
|
||||
}
|
||||
},
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
],
|
||||
});
|
||||
}));
|
||||
@@ -59,7 +51,6 @@ describe('ObjectUpdatesEffects', () => {
|
||||
action = new RemoveObjectUpdatesAction(testURL);
|
||||
});
|
||||
it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => {
|
||||
action = new RemoveObjectUpdatesAction(testURL);
|
||||
actions = hot('--a-', { a: action });
|
||||
(updatesEffects as any).actionMap$[testURL].subscribe((act) => emittedAction = act);
|
||||
const expected = cold('--b-', { b: undefined });
|
||||
@@ -81,14 +72,19 @@ describe('ObjectUpdatesEffects', () => {
|
||||
removeAction = new RemoveObjectUpdatesAction(testURL);
|
||||
});
|
||||
it('should return a RemoveObjectUpdatesAction', () => {
|
||||
actions = hot('a|', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe(
|
||||
filter(((action) => hasValue(action))))
|
||||
.subscribe((t) => {
|
||||
expect(t).toEqual(removeAction);
|
||||
}
|
||||
)
|
||||
;
|
||||
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||
|
||||
// Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not
|
||||
// keep track of the current state
|
||||
let emittedAction: Action | undefined;
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => {
|
||||
emittedAction = action;
|
||||
});
|
||||
|
||||
// This expect ensures that the mapLastActions$ was processed
|
||||
expect(updatesEffects.mapLastActions$).toBeObservable(cold('a', { a: undefined }));
|
||||
|
||||
expect(emittedAction).toEqual(removeAction);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,12 +94,24 @@ describe('ObjectUpdatesEffects', () => {
|
||||
infoNotification.options.timeOut = 10;
|
||||
});
|
||||
it('should return an action with type NO_ACTION', () => {
|
||||
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||
actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) });
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => {
|
||||
expect(t).toEqual(new NoOpAction());
|
||||
}
|
||||
);
|
||||
actions = hot('--(ab)', {
|
||||
a: new DiscardObjectUpdatesAction(testURL, infoNotification),
|
||||
b: new ReinstateObjectUpdatesAction(testURL),
|
||||
});
|
||||
|
||||
// Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not
|
||||
// keep track of the current state
|
||||
let emittedAction: Action | undefined;
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe(
|
||||
take(2)
|
||||
).subscribe((action: Action | NoOpAction) => {
|
||||
emittedAction = action;
|
||||
});
|
||||
|
||||
// This expect ensures that the mapLastActions$ was processed
|
||||
expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined }));
|
||||
|
||||
expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,12 +121,22 @@ describe('ObjectUpdatesEffects', () => {
|
||||
infoNotification.options.timeOut = 10;
|
||||
});
|
||||
it('should return a RemoveObjectUpdatesAction', () => {
|
||||
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||
actions = hot('b', { b: new RemoveFieldUpdateAction(testURL, testUUID) });
|
||||
actions = hot('--(ab)', {
|
||||
a: new DiscardObjectUpdatesAction(testURL, infoNotification),
|
||||
b: new RemoveFieldUpdateAction(testURL, testUUID),
|
||||
});
|
||||
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) =>
|
||||
expect(t).toEqual(new RemoveObjectUpdatesAction(testURL))
|
||||
);
|
||||
// Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not
|
||||
// keep track of the current state
|
||||
let emittedAction: Action | undefined;
|
||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => {
|
||||
emittedAction = action;
|
||||
});
|
||||
|
||||
// This expect ensures that the mapLastActions$ was processed
|
||||
expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined }));
|
||||
|
||||
expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
SetEditableFieldUpdateAction,
|
||||
SetValidFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer';
|
||||
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer, ObjectUpdatesState } from './object-updates.reducer';
|
||||
import { Relationship } from '../../shared/item-relationships/relationship.model';
|
||||
import { FieldChangeType } from './field-change-type.model';
|
||||
|
||||
@@ -56,7 +56,7 @@ const modDate = new Date(2010, 2, 11);
|
||||
const uuid = identifiable1.uuid;
|
||||
const url = 'test-object.url/edit';
|
||||
describe('objectUpdatesReducer', () => {
|
||||
const testState = {
|
||||
const testState: ObjectUpdatesState = {
|
||||
[url]: {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
@@ -79,9 +79,6 @@ describe('objectUpdatesReducer', () => {
|
||||
[identifiable2.uuid]: {
|
||||
field: {
|
||||
uuid: identifiable2.uuid,
|
||||
key: 'dc.titl',
|
||||
language: null,
|
||||
value: 'New title'
|
||||
},
|
||||
changeType: FieldChangeType.ADD
|
||||
}
|
||||
@@ -93,7 +90,7 @@ describe('objectUpdatesReducer', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const discardedTestState = {
|
||||
const discardedTestState: ObjectUpdatesState = {
|
||||
[url]: {
|
||||
fieldStates: {
|
||||
[identifiable1.uuid]: {
|
||||
@@ -139,9 +136,6 @@ describe('objectUpdatesReducer', () => {
|
||||
[identifiable2.uuid]: {
|
||||
field: {
|
||||
uuid: identifiable2.uuid,
|
||||
key: 'dc.titl',
|
||||
language: null,
|
||||
value: 'New title'
|
||||
},
|
||||
changeType: FieldChangeType.ADD
|
||||
}
|
||||
@@ -173,48 +167,80 @@ describe('objectUpdatesReducer', () => {
|
||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable2], modDate);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the SET_EDITABLE_FIELD action without affecting the previous state', () => {
|
||||
const action = new SetEditableFieldUpdateAction(url, uuid, false);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the ADD_FIELD action without affecting the previous state', () => {
|
||||
const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the DISCARD action without affecting the previous state', () => {
|
||||
const action = new DiscardObjectUpdatesAction(url, null);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the REINSTATE action without affecting the previous state', () => {
|
||||
const action = new ReinstateObjectUpdatesAction(url);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the REMOVE action without affecting the previous state', () => {
|
||||
const action = new RemoveFieldUpdateAction(url, uuid);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the REMOVE_FIELD action without affecting the previous state', () => {
|
||||
const action = new RemoveFieldUpdateAction(url, uuid);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => {
|
||||
const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true);
|
||||
// testState has already been frozen above
|
||||
objectUpdatesReducer(testState, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||
|
@@ -77,7 +77,7 @@ export interface DeleteRelationship extends RelationshipIdentifiable {
|
||||
*/
|
||||
export interface ObjectUpdatesEntry {
|
||||
fieldStates: FieldStates;
|
||||
fieldUpdates: FieldUpdates;
|
||||
fieldUpdates?: FieldUpdates;
|
||||
virtualMetadataSources: VirtualMetadataSources;
|
||||
lastModified: Date;
|
||||
patchOperationService?: GenericConstructor<PatchOperationService>;
|
||||
|
@@ -1,40 +1,40 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import {
|
||||
EPeopleRegistryCancelEPersonAction,
|
||||
EPeopleRegistryEditEPersonAction
|
||||
} from '../../access-control/epeople-registry/epeople-registry.actions';
|
||||
import { GroupMock } from '../../shared/testing/group-mock';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
import { PatchRequest, PostRequest } from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { EPersonDataService } from './eperson-data.service';
|
||||
import { editEPersonSelector, EPersonDataService } from './eperson-data.service';
|
||||
import { EPerson } from './models/eperson.model';
|
||||
import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
|
||||
describe('EPersonDataService', () => {
|
||||
let service: EPersonDataService;
|
||||
let store: Store<CoreState>;
|
||||
let store: MockStore<CoreState>;
|
||||
let requestService: RequestService;
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
let epeople;
|
||||
|
||||
@@ -44,50 +44,38 @@ describe('EPersonDataService', () => {
|
||||
let epeople$;
|
||||
let rdbService;
|
||||
|
||||
function initTestService() {
|
||||
return new EPersonDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
null,
|
||||
halService,
|
||||
new DummyChangeAnalyzer() as any,
|
||||
null,
|
||||
store,
|
||||
);
|
||||
}
|
||||
const initialState = {
|
||||
epeopleRegistry: {
|
||||
editEPerson: null
|
||||
},
|
||||
};
|
||||
|
||||
function init() {
|
||||
beforeEach(waitForAsync(() => {
|
||||
restEndpointURL = 'https://rest.api/dspace-spring-rest/api/eperson';
|
||||
epersonsEndpoint = `${restEndpointURL}/epersons`;
|
||||
epeople = [EPersonMock, EPersonMock2];
|
||||
epeople$ = createSuccessfulRemoteDataObject$(createPaginatedList([epeople]));
|
||||
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/dspace-spring-rest/api/eperson/epersons': epeople$ });
|
||||
halService = new HALEndpointServiceStub(restEndpointURL);
|
||||
requestService = getMockRequestService(createRequestEntry$(epeople));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot({}),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
providers: [
|
||||
EPersonDataService,
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
provideMockStore({ initialState }),
|
||||
{ provide: ObjectCacheService, useValue: {} },
|
||||
{ provide: DSOChangeAnalyzer, useClass: DummyChangeAnalyzer },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
],
|
||||
declarations: [],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
requestService = getMockRequestService(createRequestEntry$(epeople));
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
service = initTestService();
|
||||
spyOn(store, 'dispatch');
|
||||
});
|
||||
service = TestBed.inject(EPersonDataService);
|
||||
store = TestBed.inject(Store) as MockStore<CoreState>;
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
}));
|
||||
|
||||
describe('searchByScope', () => {
|
||||
beforeEach(() => {
|
||||
@@ -264,34 +252,29 @@ describe('EPersonDataService', () => {
|
||||
});
|
||||
|
||||
describe('clearEPersonRequests', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
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);
|
||||
beforeEach(() => {
|
||||
spyOn(halService, 'getEndpoint').and.callFake((linkPath: string) => {
|
||||
return observableOf(`${restEndpointURL}/${linkPath}`);
|
||||
});
|
||||
});
|
||||
it('should remove the eperson hrefs in the request service', fakeAsync(() => {
|
||||
service.clearEPersonRequests();
|
||||
tick();
|
||||
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(epersonsEndpoint);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('getActiveEPerson', () => {
|
||||
it('should retrieve the ePerson currently getting edited, if any', () => {
|
||||
service.editEPerson(EPersonMock);
|
||||
// Update the state with the ePerson (the provideMockStore doesn't update itself when dispatch is called)
|
||||
store.overrideSelector(editEPersonSelector, EPersonMock);
|
||||
|
||||
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
expect(activeEPerson).toEqual(EPersonMock);
|
||||
});
|
||||
expect(service.getActiveEPerson()).toBeObservable(cold('a', { a: EPersonMock }));
|
||||
});
|
||||
|
||||
it('should retrieve the ePerson currently getting edited, null if none being edited', () => {
|
||||
service.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
expect(activeEPerson).toEqual(null);
|
||||
});
|
||||
expect(service.getActiveEPerson()).toBeObservable(cold('a', { a: null }));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -37,7 +37,7 @@ import { dataService } from '../data/base/data-service.decorator';
|
||||
import { getEPersonEditRoute } from '../../access-control/access-control-routing-paths';
|
||||
|
||||
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
||||
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
||||
export const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
||||
|
||||
/**
|
||||
* A service to retrieve {@link EPerson}s from the REST API & EPerson related CRUD actions
|
||||
|
@@ -39,4 +39,6 @@ export enum Context {
|
||||
MyDSpaceValidation = 'mydspaceValidation',
|
||||
|
||||
Bitstream = 'bitstream',
|
||||
|
||||
CoarNotify = 'coarNotify',
|
||||
}
|
||||
|
@@ -12,14 +12,13 @@ describe('Item', () => {
|
||||
const bitstream1Path = 'document.pdf';
|
||||
const bitstream2Path = 'otherfile.doc';
|
||||
|
||||
const nonExistingBundleName = 'c1e568f7-d14e-496b-bdd7-07026998cc00';
|
||||
let bitstreams;
|
||||
let remoteDataThumbnail;
|
||||
let remoteDataThumbnailList;
|
||||
let remoteDataFiles;
|
||||
let remoteDataBundles;
|
||||
|
||||
beforeEach(() => {
|
||||
it('should be possible to create an Item without any errors', () => {
|
||||
const thumbnail = {
|
||||
content: thumbnailPath
|
||||
};
|
||||
@@ -51,5 +50,6 @@ describe('Item', () => {
|
||||
remoteDataBundles = createSuccessfulRemoteDataObject$(createPaginatedList(bundles));
|
||||
|
||||
item = Object.assign(new Item(), { bundles: remoteDataBundles });
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
@@ -105,7 +105,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
protected linkService: LinkService,
|
||||
protected halService: HALEndpointService,
|
||||
protected requestService: RequestService,
|
||||
protected rdb: RemoteDataBuildService,) {
|
||||
protected rdb: RemoteDataBuildService) {
|
||||
|
||||
this.initDefaults();
|
||||
}
|
||||
|
@@ -7,4 +7,5 @@ export enum ViewMode {
|
||||
GridElement = 'grid',
|
||||
DetailedListElement = 'detailed',
|
||||
StandalonePage = 'standalone',
|
||||
Table = 'table',
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Object model for the data returned by the REST API to present potential duplicates in a submission section
|
||||
*/
|
||||
import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model';
|
||||
|
||||
export interface WorkspaceitemSectionDuplicatesObject {
|
||||
potentialDuplicates?: Duplicate[]
|
||||
}
|
@@ -3,8 +3,9 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod
|
||||
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
|
||||
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
|
||||
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model';
|
||||
import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model';
|
||||
import { WorkspaceitemSectionIdentifiersObject } from './workspaceitem-section-identifiers.model';
|
||||
import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
|
||||
import { WorkspaceitemSectionDuplicatesObject } from './workspaceitem-section-duplicates.model';
|
||||
|
||||
/**
|
||||
* An interface to represent submission's section object.
|
||||
@@ -25,6 +26,7 @@ export type WorkspaceitemSectionDataType
|
||||
| WorkspaceitemSectionAccessesObject
|
||||
| WorkspaceitemSectionSherpaPoliciesObject
|
||||
| WorkspaceitemSectionIdentifiersObject
|
||||
| WorkspaceitemSectionDuplicatesObject
|
||||
| string;
|
||||
|
||||
|
||||
|
@@ -0,0 +1,30 @@
|
||||
import { SubmissionDuplicateDataService } from './submission-duplicate-data.service';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
|
||||
/**
|
||||
* Basic tests for the submission-duplicate-data.service.ts service
|
||||
*/
|
||||
describe('SubmissionDuplicateDataService', () => {
|
||||
const duplicateDataService = new SubmissionDuplicateDataService(null, null, null, null);
|
||||
|
||||
// Test the findDuplicates method to make sure that a call results in an expected
|
||||
// call to searchBy, using the 'findByItem' search method
|
||||
describe('findDuplicates', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(duplicateDataService, 'searchBy');
|
||||
});
|
||||
|
||||
it('should call searchBy with the correct arguments', () => {
|
||||
// Set up expected search parameters and find options
|
||||
const searchParams = [];
|
||||
searchParams.push(new RequestParam('uuid', 'test'));
|
||||
let findListOptions = new FindListOptions();
|
||||
findListOptions.searchParams = searchParams;
|
||||
// Perform test search using uuid 'test' using the findDuplicates method
|
||||
const result = duplicateDataService.findDuplicates('test', new FindListOptions(), true, true);
|
||||
// Expect searchBy('findByItem'...) to have been used as SearchData impl with the expected options (uuid=test)
|
||||
expect(duplicateDataService.searchBy).toHaveBeenCalledWith('findByItem', findListOptions, true, true);
|
||||
});
|
||||
});
|
||||
});
|
139
src/app/core/submission/submission-duplicate-data.service.ts
Normal file
139
src/app/core/submission/submission-duplicate-data.service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { GetRequest } from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { SearchResponseParsingService } from '../data/search-response-parsing.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RestRequest } from '../data/rest-request.model';
|
||||
import { BaseDataService } from '../data/base/base-data.service';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
||||
import { DUPLICATE } from '../../shared/object-list/duplicate-data/duplicate.resource-type';
|
||||
import { dataService } from '../data/base/data-service.decorator';
|
||||
|
||||
|
||||
/**
|
||||
* Service that handles search requests for potential duplicate items.
|
||||
* This uses the /api/submission/duplicates endpoint to look for other archived or in-progress items (if user
|
||||
* has READ permission) that match the item (for the given uuid).
|
||||
* Matching is configured in the backend in dspace/config/modulesduplicate-detection.cfg
|
||||
* The returned results are small preview 'stubs' of items, and displayed in either a submission section
|
||||
* or the workflow pooled/claimed task page.
|
||||
*
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(DUPLICATE)
|
||||
export class SubmissionDuplicateDataService extends BaseDataService<Duplicate> implements SearchData<Duplicate> {
|
||||
|
||||
/**
|
||||
* The ResponseParsingService constructor name
|
||||
*/
|
||||
private parser: GenericConstructor<ResponseParsingService> = SearchResponseParsingService;
|
||||
|
||||
/**
|
||||
* The RestRequest constructor name
|
||||
*/
|
||||
private request: GenericConstructor<RestRequest> = GetRequest;
|
||||
|
||||
/**
|
||||
* SearchData interface to implement
|
||||
* @private
|
||||
*/
|
||||
private searchData: SearchData<Duplicate>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
private sub;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
) {
|
||||
super('duplicates', requestService, rdbService, objectCache, halService);
|
||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the searchBy method to return paginated lists of Duplicate resources
|
||||
*
|
||||
* @param searchMethod the search method name
|
||||
* @param options find list options
|
||||
* @param useCachedVersionIfAvailable whether to use cached version if available
|
||||
* @param reRequestOnStale whether to rerequest results on stale
|
||||
* @param linksToFollow links to follow in results
|
||||
*/
|
||||
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
|
||||
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the duplicates endpoint
|
||||
* @protected
|
||||
*/
|
||||
protected getEndpoint(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set service options
|
||||
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
|
||||
* @param {boolean} request The RestRequest constructor name
|
||||
*/
|
||||
setServiceOptions(parser: GenericConstructor<ResponseParsingService>, request: GenericConstructor<RestRequest>) {
|
||||
if (parser) {
|
||||
this.parser = parser;
|
||||
}
|
||||
if (request) {
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find duplicates for a given item UUID. Locates and returns results from the /api/submission/duplicates/search/findByItem
|
||||
* SearchRestMethod, which is why this implements SearchData<Duplicate> and searchBy
|
||||
*
|
||||
* @param uuid the item UUID
|
||||
* @param options any find list options e.g. paging
|
||||
* @param useCachedVersionIfAvailable whether to use cached version if available
|
||||
* @param reRequestOnStale whether to rerequest results on stale
|
||||
* @param linksToFollow links to follow in results
|
||||
*/
|
||||
public findDuplicates(uuid: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Duplicate>[]): Observable<RemoteData<PaginatedList<Duplicate>>> {
|
||||
const searchParams = [new RequestParam('uuid', uuid)];
|
||||
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 actual search/findByItem results
|
||||
return this.searchBy('findByItem', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.sub !== undefined) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field class="mr-auto" [item]="object">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="d-flex flex-row">
|
||||
<ds-themed-item-page-title-field [item]="object" class="mr-auto">
|
||||
</ds-themed-item-page-title-field>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { isPlatformServer } from '@angular/common';
|
||||
@@ -40,6 +40,8 @@ export class HomePageComponent implements OnInit, OnDestroy {
|
||||
switchMap((coarLdnEnabled: boolean) => {
|
||||
if (coarLdnEnabled) {
|
||||
return this.notifyInfoService.getCoarLdnLocalInboxUrls();
|
||||
} else {
|
||||
return of([]);
|
||||
}
|
||||
})
|
||||
).subscribe((coarRestApiUrls: string[]) => {
|
||||
|
@@ -32,7 +32,7 @@ import { SearchConfigurationServiceStub } from '../../shared/testing/search-conf
|
||||
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { environment } from 'src/environments/environment.test';
|
||||
|
||||
describe('TopLevelCommunityList Component', () => {
|
||||
describe('TopLevelCommunityListComponent', () => {
|
||||
let comp: TopLevelCommunityListComponent;
|
||||
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
|
||||
let communityDataServiceStub: any;
|
||||
@@ -173,17 +173,17 @@ describe('TopLevelCommunityList Component', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should display a list of top-communities', () => {
|
||||
waitForAsync(() => {
|
||||
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||
it('should display a list of top-communities', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||
|
||||
expect(subComList.length).toEqual(5);
|
||||
expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1');
|
||||
expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2');
|
||||
expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3');
|
||||
expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4');
|
||||
expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5');
|
||||
});
|
||||
expect(subComList.length).toEqual(5);
|
||||
expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1');
|
||||
expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2');
|
||||
expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3');
|
||||
expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4');
|
||||
expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -18,7 +18,7 @@ describe('findSuccessfulAccordingTo', () => {
|
||||
mockItem1.isWithdrawn = true;
|
||||
|
||||
mockItem2 = new Item();
|
||||
mockItem1.isWithdrawn = false;
|
||||
mockItem2.isWithdrawn = false;
|
||||
|
||||
predicate = (rd: RemoteData<Item>) => isNotEmpty(rd.payload) ? rd.payload.isWithdrawn : false;
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe('findSuccessfulAccordingTo', () => {
|
||||
const source = hot('abcde', testRD);
|
||||
const result = source.pipe(findSuccessfulAccordingTo(predicate));
|
||||
|
||||
result.subscribe((value) => expect(value).toEqual(testRD.d));
|
||||
expect(result).toBeObservable(hot('---(d|)', { d: testRD.d }));
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="col-12">
|
||||
<h1 class="border-bottom">{{'item.edit.head' | translate}}</h1>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start" role="tablist">
|
||||
<ul *ngIf="pages.length > 0" class="nav nav-tabs justify-content-start" role="tablist">
|
||||
<li *ngFor="let page of pages" class="nav-item" role="presentation">
|
||||
<a *ngIf="(page.enabled | async)"
|
||||
[attr.aria-selected]="page.page === currentPage"
|
||||
|
@@ -191,20 +191,6 @@ describe('ItemBitstreamsComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dropBitstream is called', () => {
|
||||
const event = {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
// eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function
|
||||
finish: () => {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
comp.dropBitstream(bundle, event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dropBitstream is called', () => {
|
||||
beforeEach((done) => {
|
||||
comp.dropBitstream(bundle, {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
|
||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -16,7 +16,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
* Creates an embedded view of the contents. This is to ensure the table structure won't break.
|
||||
* (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element)
|
||||
*/
|
||||
export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The view on the bundle information and bitstreams
|
||||
@@ -67,4 +67,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
this.viewContainerRef.createEmbeddedView(this.bundleView);
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.viewContainerRef.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-edit-bitstream-drag-handle',
|
||||
@@ -10,7 +10,7 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
* Creates an embedded view of the contents
|
||||
* (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element)
|
||||
*/
|
||||
export class ItemEditBitstreamDragHandleComponent implements OnInit {
|
||||
export class ItemEditBitstreamDragHandleComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The view on the drag-handle
|
||||
*/
|
||||
@@ -23,4 +23,8 @@ export class ItemEditBitstreamDragHandleComponent implements OnInit {
|
||||
this.viewContainerRef.createEmbeddedView(this.handleView);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.viewContainerRef.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
@@ -21,7 +21,7 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
||||
* Creates an embedded view of the contents
|
||||
* (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element)
|
||||
*/
|
||||
export class ItemEditBitstreamComponent implements OnChanges, OnInit {
|
||||
export class ItemEditBitstreamComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* The view on the bitstream
|
||||
@@ -72,6 +72,10 @@ export class ItemEditBitstreamComponent implements OnChanges, OnInit {
|
||||
this.viewContainerRef.createEmbeddedView(this.bitstreamView);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.viewContainerRef.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current bitstream and its format on changes
|
||||
* @param changes
|
||||
|
@@ -37,8 +37,8 @@ describe('ItemPageFieldComponent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const buildTestEnvironment = async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
beforeEach(waitForAsync(() => {
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot({
|
||||
@@ -65,19 +65,16 @@ describe('ItemPageFieldComponent', () => {
|
||||
comp.fields = mockFields;
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
it('should display display the correct metadata value', waitForAsync(async () => {
|
||||
await buildTestEnvironment();
|
||||
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||
}));
|
||||
|
||||
describe('when markdown is disabled in the environment config', () => {
|
||||
it('should display display the correct metadata value', () => {
|
||||
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(async () => {
|
||||
describe('when markdown is disabled in the environment config', () => {
|
||||
beforeEach( () => {
|
||||
appConfig.markdown.enabled = false;
|
||||
await buildTestEnvironment();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and markdown is disabled in this component', () => {
|
||||
|
||||
@@ -105,11 +102,9 @@ describe('ItemPageFieldComponent', () => {
|
||||
});
|
||||
|
||||
describe('when markdown is enabled in the environment config', () => {
|
||||
|
||||
beforeEach(waitForAsync(async () => {
|
||||
beforeEach(() => {
|
||||
appConfig.markdown.enabled = true;
|
||||
await buildTestEnvironment();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and markdown is disabled in this component', () => {
|
||||
|
||||
@@ -139,12 +134,13 @@ describe('ItemPageFieldComponent', () => {
|
||||
|
||||
describe('test rendering of configured browse links', () => {
|
||||
beforeEach(() => {
|
||||
appConfig.markdown.enabled = false;
|
||||
comp.enableMarkdown = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
waitForAsync(() => {
|
||||
it('should have a browse link', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
|
||||
it('should have a browse link', async () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,10 +149,9 @@ describe('ItemPageFieldComponent', () => {
|
||||
comp.urlRegex = '^test';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
waitForAsync(() => {
|
||||
it('should have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
|
||||
it('should have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,14 +160,11 @@ describe('ItemPageFieldComponent', () => {
|
||||
comp.urlRegex = '^nope';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
it('should NOT have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT have a rendered (non-browse) link since the value matches ^test', () => {
|
||||
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
export function mockItemWithMetadataFieldsAndValue(fields: string[], value: string): Item {
|
||||
|
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { isPlatformServer } from '@angular/common';
|
||||
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { Observable, combineLatest, of } from 'rxjs';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
@@ -155,6 +155,8 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
||||
switchMap((coarLdnEnabled: boolean) => {
|
||||
if (coarLdnEnabled) {
|
||||
return this.notifyInfoService.getCoarLdnLocalInboxUrls();
|
||||
} else {
|
||||
return of([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="row" *ngIf="iiifEnabled">
|
||||
<div class="col-12">
|
||||
<ds-mirador-viewer id="iiif-viewer"
|
||||
@@ -86,7 +86,7 @@
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['notify.relation.endorsedBy']"
|
||||
[label]="'item.page.endorsment'">
|
||||
[label]="'item.page.endorsement'">
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['datacite.relation.isReviewedBy']"
|
||||
|
@@ -469,30 +469,31 @@ describe('ItemComponent', () => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should hide back button',() => {
|
||||
it('should hide back button', () => {
|
||||
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf('/item'));
|
||||
comp.showBackButton.subscribe((val) => {
|
||||
comp.ngOnInit();
|
||||
comp.showBackButton$.subscribe((val) => {
|
||||
expect(val).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('should show back button for search', () => {
|
||||
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(searchUrl));
|
||||
comp.ngOnInit();
|
||||
comp.showBackButton.subscribe((val) => {
|
||||
comp.showBackButton$.subscribe((val) => {
|
||||
expect(val).toBeTrue();
|
||||
});
|
||||
});
|
||||
it('should show back button for browse', () => {
|
||||
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(browseUrl));
|
||||
comp.ngOnInit();
|
||||
comp.showBackButton.subscribe((val) => {
|
||||
comp.showBackButton$.subscribe((val) => {
|
||||
expect(val).toBeTrue();
|
||||
});
|
||||
});
|
||||
it('should show back button for recent submissions', () => {
|
||||
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(recentSubmissionsUrl));
|
||||
comp.ngOnInit();
|
||||
comp.showBackButton.subscribe((val) => {
|
||||
comp.showBackButton$.subscribe((val) => {
|
||||
expect(val).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@ import { getItemPageRoute } from '../../../item-page-routing-paths';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif-utils';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
@@ -27,7 +27,7 @@ export class ItemComponent implements OnInit {
|
||||
/**
|
||||
* Used to show or hide the back to results button in the view.
|
||||
*/
|
||||
showBackButton: Observable<boolean>;
|
||||
showBackButton$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Route to the item page
|
||||
@@ -73,10 +73,9 @@ export class ItemComponent implements OnInit {
|
||||
|
||||
this.itemPageRoute = getItemPageRoute(this.object);
|
||||
// hide/show the back button
|
||||
this.showBackButton = this.routeService.getPreviousUrl().pipe(
|
||||
filter(url => this.previousRoute.test(url)),
|
||||
this.showBackButton$ = this.routeService.getPreviousUrl().pipe(
|
||||
map((url: string) => this.previousRoute.test(url)),
|
||||
take(1),
|
||||
map(() => true)
|
||||
);
|
||||
// check to see if iiif viewer is required.
|
||||
this.iiifEnabled = isIiifEnabled(this.object);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-themed-results-back-button *ngIf="showBackButton | async" [back]="back"></ds-themed-results-back-button>
|
||||
<ds-themed-results-back-button *ngIf="showBackButton$ | async" [back]="back"></ds-themed-results-back-button>
|
||||
<div class="row" *ngIf="iiifEnabled">
|
||||
<div class="col-12">
|
||||
<ds-mirador-viewer id="iiif-viewer"
|
||||
@@ -72,7 +72,7 @@
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['notify.relation.endorsedBy']"
|
||||
[label]="'item.page.endorsment'">
|
||||
[label]="'item.page.endorsement'">
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['datacite.relation.isReviewedBy']"
|
||||
|
@@ -4,7 +4,7 @@ import {
|
||||
} from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Version } from '../../core/shared/version.model';
|
||||
import { VersionHistory } from '../../core/shared/version-history.model';
|
||||
@@ -220,20 +220,21 @@ describe('ItemVersionsComponent', () => {
|
||||
authorizationServiceSpy.isAuthorized.and.callFake(canDelete);
|
||||
}));
|
||||
it('should not disable the delete button', () => {
|
||||
const deleteButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-delete`));
|
||||
deleteButtons.forEach((btn) => {
|
||||
const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete'));
|
||||
expect(deleteButtons.length).not.toBe(0);
|
||||
deleteButtons.forEach((btn: DebugElement) => {
|
||||
expect(btn.nativeElement.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should disable other buttons', () => {
|
||||
const createButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
||||
createButtons.forEach((btn) => {
|
||||
expect(btn.nativeElement.disabled).toBe(true);
|
||||
});
|
||||
const editButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
|
||||
editButtons.forEach((btn) => {
|
||||
expect(btn.nativeElement.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide the create buttons', () => {
|
||||
const createButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-create'));
|
||||
expect(createButtons.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should hide the edit buttons', () => {
|
||||
const editButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-edit'));
|
||||
expect(editButtons.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -368,18 +368,40 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
icon: 'terminal',
|
||||
index: 10
|
||||
},
|
||||
/* COAR Notify section */
|
||||
{
|
||||
id: 'coar_notify',
|
||||
active: false,
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.coar_notify'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'inbox',
|
||||
index: 13
|
||||
},
|
||||
{
|
||||
id: 'notify_dashboard',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.notify_dashboard',
|
||||
link: '/admin/notify-dashboard'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* LDN Services */
|
||||
{
|
||||
id: 'ldn_services',
|
||||
active: false,
|
||||
parentID: 'coar_notify',
|
||||
visible: isSiteAdmin && isCoarNotifyEnabled,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.services',
|
||||
link: '/admin/ldn/services'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'inbox',
|
||||
index: 14
|
||||
},
|
||||
{
|
||||
id: 'health',
|
||||
|
@@ -5,7 +5,6 @@ import { By } from '@angular/platform-browser';
|
||||
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -48,10 +47,10 @@ import { SharedModule } from '../shared.module';
|
||||
import { BrowseByRoutingModule } from '../../browse-by/browse-by-routing.module';
|
||||
import { AccessControlRoutingModule } from '../../access-control/access-control-routing.module';
|
||||
|
||||
@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom')
|
||||
@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'dspace')
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
selector: 'ds-browse-entry-list-element',
|
||||
template: ''
|
||||
})
|
||||
class MockThemedBrowseEntryListElementComponent {
|
||||
@@ -61,28 +60,6 @@ describe('BrowseByComponent', () => {
|
||||
let comp: BrowseByComponent;
|
||||
let fixture: ComponentFixture<BrowseByComponent>;
|
||||
|
||||
const mockItems = [
|
||||
Object.assign(new Item(), {
|
||||
id: 'fakeId-1',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'First Fake Title'
|
||||
}
|
||||
]
|
||||
}),
|
||||
Object.assign(new Item(), {
|
||||
id: 'fakeId-2',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'Second Fake Title'
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
const mockItemsRD$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockItems));
|
||||
|
||||
const groupDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: '',
|
||||
@@ -113,8 +90,8 @@ describe('BrowseByComponent', () => {
|
||||
let themeService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeService = getMockThemeService('dspace');
|
||||
TestBed.configureTestingModule({
|
||||
themeService = getMockThemeService('base');
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowseByRoutingModule,
|
||||
AccessControlRoutingModule,
|
||||
@@ -200,40 +177,40 @@ describe('BrowseByComponent', () => {
|
||||
});
|
||||
|
||||
describe('when theme is base', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
themeService.getThemeName.and.returnValue('base');
|
||||
themeService.getThemeName$.and.returnValue(observableOf('base'));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should use the base component to render browse entries', () => {
|
||||
waitForAsync(() => {
|
||||
const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent));
|
||||
expect(componentLoaders.length).toEqual(browseEntries.length);
|
||||
componentLoaders.forEach((componentLoader) => {
|
||||
const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element'));
|
||||
expect(browseEntry.componentInstance).toBeInstanceOf(BrowseEntryListElementComponent);
|
||||
});
|
||||
const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent));
|
||||
expect(componentLoaders.length).toEqual(browseEntries.length);
|
||||
componentLoaders.forEach((componentLoader) => {
|
||||
const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element'));
|
||||
expect(browseEntry.componentInstance).toBeInstanceOf(BrowseEntryListElementComponent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when theme is custom', () => {
|
||||
beforeEach(() => {
|
||||
themeService.getThemeName.and.returnValue('custom');
|
||||
themeService.getThemeName$.and.returnValue(observableOf('custom'));
|
||||
describe('when theme is dspace', () => {
|
||||
beforeEach(async () => {
|
||||
themeService.getThemeName.and.returnValue('dspace');
|
||||
themeService.getThemeName$.and.returnValue(observableOf('dspace'));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should use the themed component to render browse entries', () => {
|
||||
waitForAsync(() => {
|
||||
const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent));
|
||||
expect(componentLoaders.length).toEqual(browseEntries.length);
|
||||
componentLoaders.forEach((componentLoader) => {
|
||||
const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element'));
|
||||
expect(browseEntry.componentInstance).toBeInstanceOf(MockThemedBrowseEntryListElementComponent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -27,4 +27,5 @@ const DECLARATIONS = [
|
||||
...DECLARATIONS,
|
||||
]
|
||||
})
|
||||
export class SharedBrowseByModule { }
|
||||
export class SharedBrowseByModule {
|
||||
}
|
||||
|
@@ -24,9 +24,8 @@ import { Community } from '../../core/shared/community.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import flatten from 'lodash/flatten';
|
||||
import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
|
||||
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||
import { AuthServiceMock } from '../mocks/auth.service.mock';
|
||||
import { CorrectionTypeDataService } from 'src/app/core/submission/correctiontype-data.service';
|
||||
import { createPaginatedList } from '../testing/utils.test';
|
||||
|
||||
describe('DSOEditMenuResolver', () => {
|
||||
|
||||
@@ -152,7 +151,7 @@ describe('DSOEditMenuResolver', () => {
|
||||
});
|
||||
|
||||
correctionsDataService = jasmine.createSpyObj('correctionsDataService', {
|
||||
findByItem: observableOf([])
|
||||
findByItem: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -167,7 +166,6 @@ describe('DSOEditMenuResolver', () => {
|
||||
{provide: TranslateService, useValue: translate},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService},
|
||||
{provide: AuthService, useValue: new AuthServiceMock()},
|
||||
{provide: CorrectionTypeDataService, useValue: correctionsDataService},
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
@@ -367,7 +365,7 @@ describe('DSOEditMenuResolver', () => {
|
||||
route = dsoRoute(testItem);
|
||||
});
|
||||
|
||||
it('should return Item-specific entries', () => {
|
||||
it('should return Item-specific entries', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const orcidEntry = menu.find(entry => entry.id === 'orcid-dso');
|
||||
@@ -388,18 +386,20 @@ describe('DSOEditMenuResolver', () => {
|
||||
expect(claimEntry.active).toBeFalse();
|
||||
expect(claimEntry.visible).toBeFalse();
|
||||
expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return Community/Collection-specific entries', () => {
|
||||
it('should not return Community/Collection-specific entries', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const subscribeEntry = menu.find(entry => entry.id === 'subscribe');
|
||||
expect(subscribeEntry).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return as third part the common list ', () => {
|
||||
it('should return as third part the common list ', (done: DoneFn) => {
|
||||
const result = resolver.getDsoMenus(testObject, route, state);
|
||||
combineLatest(result).pipe(map(flatten)).subscribe((menu) => {
|
||||
const editEntry = menu.find(entry => entry.id === 'edit-dso');
|
||||
@@ -410,6 +410,7 @@ describe('DSOEditMenuResolver', () => {
|
||||
expect((editEntry.model as LinkMenuItemModel).link).toEqual(
|
||||
'/items/test-item-uuid/edit/metadata'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -24,9 +24,6 @@ import { ResearcherProfileDataService } from '../../core/profile/researcher-prof
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DsoWithdrawnReinstateModalService, REQUEST_REINSTATE, REQUEST_WITHDRAWN } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
import { RequestParam } from '../../core/cache/models/request-param.model';
|
||||
import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service';
|
||||
import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
@@ -50,7 +47,6 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
|
||||
private auth: AuthService,
|
||||
private correctionTypeDataService: CorrectionTypeDataService
|
||||
) {
|
||||
}
|
||||
@@ -133,9 +129,6 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
|
||||
*/
|
||||
protected getItemMenu(dso): Observable<MenuSection[]> {
|
||||
if (dso instanceof Item) {
|
||||
const findListTopicOptions: FindListOptions = {
|
||||
searchParams: [new RequestParam('target', dso.uuid)]
|
||||
};
|
||||
return combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self),
|
||||
this.dsoVersioningModalService.isNewVersionButtonDisabled(dso),
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||
<span *ngIf="!(canDownload$ |async)" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
||||
<span role="img" *ngIf="!(canDownload$ |async)" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
@@ -33,6 +33,12 @@ export class VocabularyTreeviewModalComponent {
|
||||
*/
|
||||
@Input() multiSelect = false;
|
||||
|
||||
/**
|
||||
* An event fired when a vocabulary entry is selected.
|
||||
* Event's payload equals to {@link VocabularyEntryDetail} selected.
|
||||
*/
|
||||
@Output() select: EventEmitter<VocabularyEntryDetail> = new EventEmitter<VocabularyEntryDetail>(null);
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
@@ -46,6 +52,7 @@ export class VocabularyTreeviewModalComponent {
|
||||
* Method called on entry select
|
||||
*/
|
||||
onSelect(item: VocabularyEntryDetail) {
|
||||
this.select.emit(item);
|
||||
this.activeModal.close(item);
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,10 @@ describe('hostWindowReducer', () => {
|
||||
|
||||
const action = new HostWindowResizeAction(1024, 768);
|
||||
hostWindowReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -180,6 +180,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set collapsed to false for the correct menu in response to the EXPAND_MENU action', () => {
|
||||
@@ -201,6 +202,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set collapsed to false for the correct menu in response to the TOGGLE_MENU action when collapsed is true', () => {
|
||||
@@ -231,6 +233,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set previewCollapsed to true for the correct menu in response to the COLLAPSE_MENU_PREVIEW action', () => {
|
||||
@@ -252,6 +255,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set previewCollapsed to false for the correct menu in response to the EXPAND_MENU_PREVIEW action', () => {
|
||||
@@ -273,6 +277,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set visible to true for the correct menu in response to the SHOW_MENU action', () => {
|
||||
@@ -294,6 +299,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set previewCollapsed to false for the correct menu in response to the HIDE_MENU action', () => {
|
||||
@@ -315,6 +321,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should reset the menu state to the initial state when performing the REINIT_MENUS action without affecting the previous state', () => {
|
||||
@@ -358,6 +365,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should remove a section for the correct menu in response to the REMOVE_SECTION action', () => {
|
||||
@@ -394,6 +402,10 @@ describe('menusReducer', () => {
|
||||
|
||||
const action = new ActivateMenuSectionAction(menuID, topSectionID);
|
||||
menusReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set active to false for the correct menu section in response to the DEACTIVATE_SECTION action', () => {
|
||||
@@ -412,6 +424,10 @@ describe('menusReducer', () => {
|
||||
|
||||
const action = new DeactivateMenuSectionAction(menuID, topSectionID);
|
||||
menusReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set active to false for the correct menu in response to the TOGGLE_ACTIVE_SECTION action when active is true', () => {
|
||||
@@ -441,6 +457,7 @@ describe('menusReducer', () => {
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set visible to true for the correct menu section in response to the SHOW_SECTION action', () => {
|
||||
@@ -459,6 +476,10 @@ describe('menusReducer', () => {
|
||||
|
||||
const action = new ShowMenuSectionAction(menuID, topSectionID);
|
||||
menusReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should set visible to false for the correct menu section in response to the HIDE_SECTION action', () => {
|
||||
@@ -477,5 +498,9 @@ describe('menusReducer', () => {
|
||||
|
||||
const action = new HideMenuSectionAction(menuID, topSectionID);
|
||||
menusReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
@@ -1114,7 +1114,10 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, {
|
||||
isLoading: false,
|
||||
isValid: false,
|
||||
removePending: false
|
||||
} as any
|
||||
} as any,
|
||||
'duplicates': {
|
||||
potentialDuplicates: []
|
||||
} as any,
|
||||
},
|
||||
isLoading: false,
|
||||
savePending: false,
|
||||
|
@@ -0,0 +1,12 @@
|
||||
<div role="button"
|
||||
class="w-100 h-100 pt-4 pb-3 px-2 box-container"
|
||||
[ngStyle]="{'background-color': boxConfig.color}"
|
||||
[dsHoverClass]="'shadow-lg'"
|
||||
(click)="onClick(boxConfig)"
|
||||
[title]="boxConfig.description | translate"
|
||||
>
|
||||
<div [ngStyle]="{'color': boxConfig.textColor}" class="d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="mb-3 font-weight-bold box-counter">{{ boxConfig.count ?? 0 }}</div>
|
||||
<div class="font-weight-bold d-flex justify-content-center w-100">{{ boxConfig.title | translate }}</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,8 @@
|
||||
.box-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.box-counter {
|
||||
font-size: calc(var(--bs-font-size-lg) * 1.5);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user