mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merged in CST-10642-notify-logs (pull request #1183)
CST-10642 notify logs Approved-by: Stefano Maffei
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
path: '',
|
||||||
|
component: AdminNotifyDashboardComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'admin.notify.dashboard.page.title',
|
||||||
|
breadcrumbKey: 'admin.notify.dashboard',
|
||||||
|
showBreadcrumbsFluid: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'inbound',
|
||||||
|
component: AdminNotifyIncomingComponent,
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'admin.notify.dashboard.page.title',
|
||||||
|
breadcrumbKey: 'admin.notify.dashboard',
|
||||||
|
showBreadcrumbsFluid: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'outbound',
|
||||||
|
component: AdminNotifyOutgoingComponent,
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'admin.notify.dashboard.page.title',
|
||||||
|
breadcrumbKey: 'admin.notify.dashboard',
|
||||||
|
showBreadcrumbsFluid: false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Routing module for the Notifications section of the admin sidebar
|
||||||
|
*/
|
||||||
|
export class AdminNotifyDashboardRoutingModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||||
|
<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,91 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
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,45 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
SharedModule,
|
||||||
|
AdminNotifyDashboardRoutingModule,
|
||||||
|
SearchModule,
|
||||||
|
SearchPageModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AdminNotifyMessagesService,
|
||||||
|
DatePipe
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AdminNotifyDashboardComponent,
|
||||||
|
AdminNotifyMetricsComponent,
|
||||||
|
AdminNotifyIncomingComponent,
|
||||||
|
AdminNotifyOutgoingComponent,
|
||||||
|
AdminNotifySearchResultComponent,
|
||||||
|
AdminNotifyDetailModalComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminNotifyDashboardModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
<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]} }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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,35 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-notify-detail-modal',
|
||||||
|
templateUrl: './admin-notify-detail-modal.component.html',
|
||||||
|
})
|
||||||
|
export class AdminNotifyDetailModalComponent {
|
||||||
|
@Input() notifyMessage: AdminNotifyMessage;
|
||||||
|
@Input() notifyMessageKeys: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the modal is closed
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
response = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||||
|
<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'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="col-12 text-left h4 my-4">{{'admin.notify.dashboard.inbound' | translate}}</div>
|
||||||
|
<ds-themed-search
|
||||||
|
[configuration]="'NOTIFY.incoming'"
|
||||||
|
[showViewModes]="false"
|
||||||
|
[searchEnabled]="false"
|
||||||
|
[context]="context"
|
||||||
|
></ds-themed-search>
|
||||||
|
</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,21 @@
|
|||||||
|
import { Component, Inject } 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';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-notify-incoming',
|
||||||
|
templateUrl: './admin-notify-incoming.component.html',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminNotifyIncomingComponent {
|
||||||
|
protected readonly context = Context.CoarNotify;
|
||||||
|
constructor(@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="border-bottom pb-2">{{'admin-notify-dashboard.title'| translate}}</h2>
|
||||||
|
<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'}">{{'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>
|
||||||
|
|
||||||
|
<div class="col-12 text-left h4 my-4">{{'admin.notify.dashboard.outbound' | translate}}</div>
|
||||||
|
<ds-themed-search
|
||||||
|
[configuration]="'NOTIFY.outgoing'"
|
||||||
|
[showViewModes]="false"
|
||||||
|
[searchEnabled]="false"
|
||||||
|
[context]="context"
|
||||||
|
></ds-themed-search>
|
||||||
|
</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,22 @@
|
|||||||
|
import { Component, Inject } 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';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-notify-outgoing',
|
||||||
|
templateUrl: './admin-notify-outgoing.component.html',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminNotifyOutgoingComponent {
|
||||||
|
protected readonly context = Context.CoarNotify;
|
||||||
|
|
||||||
|
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 [boxConfig]="box"></ds-notification-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminNotifyMetricsComponent } from './admin-notify-metrics.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('AdminNotifyMetricsComponent', () => {
|
||||||
|
let component: AdminNotifyMetricsComponent;
|
||||||
|
let fixture: ComponentFixture<AdminNotifyMetricsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [ AdminNotifyMetricsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AdminNotifyMetricsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-notify-metrics',
|
||||||
|
templateUrl: './admin-notify-metrics.component.html',
|
||||||
|
})
|
||||||
|
export class AdminNotifyMetricsComponent {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
boxesConfig: AdminNotifyMetricsRow[];
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
export interface AdminNotifyMetricsBox {
|
||||||
|
color: string;
|
||||||
|
textColor?: string;
|
||||||
|
title: string;
|
||||||
|
config: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminNotifyMetricsRow {
|
||||||
|
title: string;
|
||||||
|
boxes: AdminNotifyMetricsBox[]
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
<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>{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="'/items/' + (isInbound ? message.context : message.object)">{{ message.relatedItem }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>{{ message.ldnService }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>{{ message.activityStreamType }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>{{ '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" (click)="reprocessMessage(message)" class="btn btn-warning">
|
||||||
|
{{ 'notify-message-result.reprocess' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@@ -0,0 +1,5 @@
|
|||||||
|
.table-responsive {
|
||||||
|
th, td {
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,177 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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'
|
||||||
|
}
|
||||||
|
] 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
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AdminNotifySearchResultComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
modalService = (component as any).modalService;
|
||||||
|
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
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,137 @@
|
|||||||
|
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',
|
||||||
|
styleUrls: ['./admin-notify-search-result.component.scss'],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 === 'NOTIFY.incoming';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open modal for details visualization
|
||||||
|
* @param message the message to be displayed
|
||||||
|
*/
|
||||||
|
openDetailModal(message: AdminNotifyMessage) {
|
||||||
|
const modalRef = this.modalService.open(AdminNotifyDetailModalComponent);
|
||||||
|
const messageToOpen = {...message};
|
||||||
|
// we delete not necessary or not readable keys
|
||||||
|
delete messageToOpen.target;
|
||||||
|
delete messageToOpen.object;
|
||||||
|
delete messageToOpen.context;
|
||||||
|
delete messageToOpen.origin;
|
||||||
|
delete messageToOpen._links;
|
||||||
|
delete messageToOpen.metadata;
|
||||||
|
delete messageToOpen.thumbnail;
|
||||||
|
delete messageToOpen.item;
|
||||||
|
delete messageToOpen.accessStatus;
|
||||||
|
delete messageToOpen.queueStatus;
|
||||||
|
|
||||||
|
const messageKeys = Object.keys(messageToOpen);
|
||||||
|
messageKeys.forEach(key => {
|
||||||
|
if (this.dateTypeKeys.includes(key)) {
|
||||||
|
messageToOpen[key] = this.datePipe.transform(messageToOpen[key], this.dateFormat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalRef.componentInstance.notifyMessage = messageToOpen;
|
||||||
|
modalRef.componentInstance.notifyMessageKeys = 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,147 @@
|
|||||||
|
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 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 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,97 @@
|
|||||||
|
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 { GetRequest } 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 GetRequest(requestId, endpointURL)),
|
||||||
|
tap(request => this.requestService.send(request)),
|
||||||
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<AdminNotifyMessage>(request.uuid)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
).pipe(
|
||||||
|
mergeMap((newMessage) => messageSubject.pipe(
|
||||||
|
map(messages => {
|
||||||
|
const messageToUpdate = messages.find(currentMessage => currentMessage.id === message.id);
|
||||||
|
const indexOfMessageToUpdate = messages.indexOf(messageToUpdate);
|
||||||
|
newMessage.target = messageToUpdate.target;
|
||||||
|
newMessage.object = messageToUpdate.object;
|
||||||
|
newMessage.origin = messageToUpdate.origin;
|
||||||
|
newMessage.context = messageToUpdate.context;
|
||||||
|
messages[indexOfMessageToUpdate] = newMessage;
|
||||||
|
return messages;
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,8 @@ import { getAdminModuleRoute } from '../app-routing-paths';
|
|||||||
export const REGISTRIES_MODULE_PATH = 'registries';
|
export const REGISTRIES_MODULE_PATH = 'registries';
|
||||||
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
|
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
|
||||||
|
|
||||||
|
export const NOTIFY_DASHBOARD_MODULE_PATH = 'notify-dashboard';
|
||||||
|
|
||||||
export const LDN_PATH = 'ldn';
|
export const LDN_PATH = 'ldn';
|
||||||
|
|
||||||
export function getRegistriesModuleRoute() {
|
export function getRegistriesModuleRoute() {
|
||||||
|
@@ -6,7 +6,12 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
import { LDN_PATH, REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths';
|
import {
|
||||||
|
LDN_PATH,
|
||||||
|
REGISTRIES_MODULE_PATH,
|
||||||
|
NOTIFICATIONS_MODULE_PATH,
|
||||||
|
NOTIFY_DASHBOARD_MODULE_PATH
|
||||||
|
} from './admin-routing-paths';
|
||||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -69,6 +74,11 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
|
|||||||
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
|
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
|
||||||
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
|
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: NOTIFY_DASHBOARD_MODULE_PATH,
|
||||||
|
loadChildren: () => import('./admin-notify-dashboard/admin-notify-dashboard.module')
|
||||||
|
.then((m) => m.AdminNotifyDashboardModule),
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -197,6 +197,7 @@ import {
|
|||||||
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config';
|
||||||
import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model';
|
import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model';
|
||||||
import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service';
|
import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service';
|
||||||
|
import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -283,7 +284,6 @@ const PROVIDERS = [
|
|||||||
SearchService,
|
SearchService,
|
||||||
SidebarService,
|
SidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
SearchFilterService,
|
|
||||||
SearchConfigurationService,
|
SearchConfigurationService,
|
||||||
SelectableListService,
|
SelectableListService,
|
||||||
RelationshipTypeDataService,
|
RelationshipTypeDataService,
|
||||||
@@ -410,6 +410,7 @@ export const models =
|
|||||||
Itemfilter,
|
Itemfilter,
|
||||||
SubmissionCoarNotifyConfig,
|
SubmissionCoarNotifyConfig,
|
||||||
NotifyRequestsStatus,
|
NotifyRequestsStatus,
|
||||||
|
AdminNotifyMessage
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -39,4 +39,6 @@ export enum Context {
|
|||||||
MyDSpaceValidation = 'mydspaceValidation',
|
MyDSpaceValidation = 'mydspaceValidation',
|
||||||
|
|
||||||
Bitstream = 'bitstream',
|
Bitstream = 'bitstream',
|
||||||
|
|
||||||
|
CoarNotify = 'coarNotify',
|
||||||
}
|
}
|
||||||
|
@@ -105,7 +105,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdb: RemoteDataBuildService,) {
|
protected rdb: RemoteDataBuildService) {
|
||||||
|
|
||||||
this.initDefaults();
|
this.initDefaults();
|
||||||
}
|
}
|
||||||
|
@@ -7,4 +7,5 @@ export enum ViewMode {
|
|||||||
GridElement = 'grid',
|
GridElement = 'grid',
|
||||||
DetailedListElement = 'detailed',
|
DetailedListElement = 'detailed',
|
||||||
StandalonePage = 'standalone',
|
StandalonePage = 'standalone',
|
||||||
|
Table = 'table',
|
||||||
}
|
}
|
||||||
|
@@ -362,19 +362,6 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
icon: 'terminal',
|
icon: 'terminal',
|
||||||
index: 10
|
index: 10
|
||||||
},
|
},
|
||||||
/* LDN Services */
|
|
||||||
{
|
|
||||||
id: 'ldn_services',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.services',
|
|
||||||
link: '/admin/ldn/services'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'inbox',
|
|
||||||
index: 14
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'health',
|
id: 'health',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -679,6 +666,41 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
icon: 'exclamation-circle',
|
icon: 'exclamation-circle',
|
||||||
index: 12
|
index: 12
|
||||||
},
|
},
|
||||||
|
/* COAR Notify section */
|
||||||
|
{
|
||||||
|
id: 'coar_notify',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.coar_notify'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'inbox',
|
||||||
|
index: 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notify_dashboard',
|
||||||
|
active: false,
|
||||||
|
parentID: 'coar_notify',
|
||||||
|
visible: authorized,
|
||||||
|
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: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.services',
|
||||||
|
link: '/admin/ldn/services'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, {
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="w-100 h-100 pt-4 pb-3 px-2 box-container" [ngStyle]="{'background-color': boxConfig.color}">
|
||||||
|
<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,7 @@
|
|||||||
|
.box-container {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
.box-counter {
|
||||||
|
font-size: calc(var(--bs-font-size-lg) * 1.5);
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,37 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationBoxComponent } from './notification-box.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
AdminNotifyMetricsBox
|
||||||
|
} from '../../admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||||
|
|
||||||
|
describe('NotificationBoxComponent', () => {
|
||||||
|
let component: NotificationBoxComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationBoxComponent>;
|
||||||
|
let mockBoxConfig: AdminNotifyMetricsBox;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockBoxConfig = {
|
||||||
|
'color': '#D4EDDA',
|
||||||
|
'title': 'admin-notify-dashboard.delivered',
|
||||||
|
'config': 'NOTIFY.outgoing.delivered',
|
||||||
|
'count': 79
|
||||||
|
};
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [ NotificationBoxComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(NotificationBoxComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.boxConfig = mockBoxConfig;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AdminNotifyMetricsBox
|
||||||
|
} from '../../admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||||
|
import { listableObjectComponent } from '../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import {
|
||||||
|
AdminNotifySearchResult
|
||||||
|
} from '../../admin/admin-notify-dashboard/models/admin-notify-message-search-result.model';
|
||||||
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
|
||||||
|
@listableObjectComponent(AdminNotifySearchResult, ViewMode.ListElement)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-notification-box',
|
||||||
|
templateUrl: './notification-box.component.html',
|
||||||
|
styleUrls: ['./notification-box.component.scss']
|
||||||
|
})
|
||||||
|
export class NotificationBoxComponent {
|
||||||
|
@Input() boxConfig: AdminNotifyMetricsBox;
|
||||||
|
}
|
@@ -58,3 +58,19 @@
|
|||||||
</ds-object-detail>
|
</ds-object-detail>
|
||||||
|
|
||||||
|
|
||||||
|
<ds-object-table [config]="config"
|
||||||
|
[sortConfig]="sortConfig"
|
||||||
|
[objects]="objects"
|
||||||
|
[hideGear]="hideGear"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[context]="context"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
|
[showPaginator]="showPaginator"
|
||||||
|
[showThumbnails]="showThumbnails"
|
||||||
|
(paginationChange)="onPaginationChange($event)"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
|
*ngIf="(currentMode$ | async) === viewModeEnum.Table">
|
||||||
|
</ds-object-table>
|
||||||
|
@@ -18,7 +18,7 @@ const testType = 'TestType';
|
|||||||
const testContext = Context.Search;
|
const testContext = Context.Search;
|
||||||
const testViewMode = ViewMode.StandalonePage;
|
const testViewMode = ViewMode.StandalonePage;
|
||||||
|
|
||||||
class TestType extends ListableObject {
|
export class TestType extends ListableObject {
|
||||||
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
|
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
|
||||||
return [testType];
|
return [testType];
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@ export const DEFAULT_THEME = '*';
|
|||||||
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
|
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
|
||||||
* - { level: 1, relevancy: 1 } is more relevant than null
|
* - { level: 1, relevancy: 1 } is more relevant than null
|
||||||
*/
|
*/
|
||||||
class MatchRelevancy {
|
export class MatchRelevancy {
|
||||||
constructor(public match: any,
|
constructor(public match: any,
|
||||||
public level: number,
|
public level: number,
|
||||||
public relevancy: number) {
|
public relevancy: number) {
|
||||||
@@ -133,7 +133,7 @@ export function getListableObjectComponent(types: (string | GenericConstructor<L
|
|||||||
* @param defaults the default values to use for each level, in case no value is found for the key at that index
|
* @param defaults the default values to use for each level, in case no value is found for the key at that index
|
||||||
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
|
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
|
||||||
*/
|
*/
|
||||||
function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
|
export function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
|
||||||
let currentMap = typeMap;
|
let currentMap = typeMap;
|
||||||
let level = -1;
|
let level = -1;
|
||||||
let relevancy = 0;
|
let relevancy = 0;
|
||||||
|
@@ -0,0 +1,77 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-objects-collection-tabulatable',
|
||||||
|
template: ``,
|
||||||
|
})
|
||||||
|
export class AbstractTabulatableElementComponent<T extends PaginatedList<ListableObject>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object to render in this list element
|
||||||
|
*/
|
||||||
|
@Input() objects: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link type to determine the type of link rendered in this element
|
||||||
|
*/
|
||||||
|
@Input() linkType: CollectionElementLinkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the list this element resides in
|
||||||
|
*/
|
||||||
|
@Input() listID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to display for this element
|
||||||
|
*/
|
||||||
|
@Input() value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the badge label or not
|
||||||
|
*/
|
||||||
|
@Input() showLabel = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the thumbnail preview
|
||||||
|
*/
|
||||||
|
@Input() showThumbnails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context we matched on to get this component
|
||||||
|
*/
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The viewmode we matched on to get this component
|
||||||
|
*/
|
||||||
|
@Input() viewMode: ViewMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit when the object has been reloaded.
|
||||||
|
*/
|
||||||
|
@Output() reloadedObject = new EventEmitter<RemoteData<PaginatedList<ListableObject>>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available link types
|
||||||
|
*/
|
||||||
|
linkTypes = CollectionElementLinkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available view modes
|
||||||
|
*/
|
||||||
|
viewModes = ViewMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available contexts
|
||||||
|
*/
|
||||||
|
contexts = Context;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1 @@
|
|||||||
|
<ng-template dsTabulatableObjects></ng-template>
|
@@ -0,0 +1,59 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TabulatableObjectsLoaderComponent } from './tabulatable-objects-loader.component';
|
||||||
|
import { ThemeService } from '../../../theme-support/theme.service';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { TabulatableObjectsDirective } from './tabulatable-objects.directive';
|
||||||
|
import { ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
TabulatableResultListElementsComponent
|
||||||
|
} from '../../../object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||||
|
import { TestType } from '../listable-object/listable-object-component-loader.component.spec';
|
||||||
|
|
||||||
|
const testType = 'TestType';
|
||||||
|
|
||||||
|
class TestTypes extends PaginatedList<ListableObject> {
|
||||||
|
page: TestType[] = [new TestType()];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe('TabulatableObjectsLoaderComponent', () => {
|
||||||
|
let component: TabulatableObjectsLoaderComponent;
|
||||||
|
let fixture: ComponentFixture<TabulatableObjectsLoaderComponent>;
|
||||||
|
|
||||||
|
let themeService: ThemeService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
themeService = jasmine.createSpyObj('themeService', {
|
||||||
|
getThemeName: 'dspace',
|
||||||
|
});
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ TabulatableObjectsLoaderComponent, TabulatableObjectsDirective ],
|
||||||
|
providers: [
|
||||||
|
provideMockStore({}),
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
|
]
|
||||||
|
}).overrideComponent(TabulatableObjectsLoaderComponent, {
|
||||||
|
set: {
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
|
entryComponents: [TabulatableResultListElementsComponent]
|
||||||
|
}
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TabulatableObjectsLoaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.objects = new TestTypes();
|
||||||
|
component.context = Context.Search;
|
||||||
|
spyOn(component, 'getComponent').and.returnValue(TabulatableResultListElementsComponent as any);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,206 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ComponentRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||||
|
import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
|
import { ThemeService } from '../../../theme-support/theme.service';
|
||||||
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
|
import { TabulatableObjectsDirective } from './tabulatable-objects.directive';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
import { getTabulatableObjectsComponent } from './tabulatable-objects.decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-tabulatable-objects-loader',
|
||||||
|
templateUrl: './tabulatable-objects-loader.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
|
||||||
|
*/
|
||||||
|
export class TabulatableObjectsLoaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
/**
|
||||||
|
* The items to determine the component for
|
||||||
|
*/
|
||||||
|
@Input() objects: PaginatedList<ListableObject>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context of listable object
|
||||||
|
*/
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of link used to render the links inside the listable object
|
||||||
|
*/
|
||||||
|
@Input() linkType: CollectionElementLinkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the list this element resides in
|
||||||
|
*/
|
||||||
|
@Input() listID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the badge label or not
|
||||||
|
*/
|
||||||
|
@Input() showLabel = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the thumbnail preview
|
||||||
|
*/
|
||||||
|
@Input() showThumbnails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to display for this element
|
||||||
|
*/
|
||||||
|
@Input() value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive hook used to place the dynamic child component
|
||||||
|
*/
|
||||||
|
@ViewChild(TabulatableObjectsDirective, { static: true }) tabulatableObjectsDirective: TabulatableObjectsDirective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit when the listable object has been reloaded.
|
||||||
|
*/
|
||||||
|
@Output() contentChange = new EventEmitter<PaginatedList<ListableObject>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
protected subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reference to the dynamic component
|
||||||
|
*/
|
||||||
|
protected compRef: ComponentRef<Component>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view mode used to identify the components
|
||||||
|
*/
|
||||||
|
protected viewMode: ViewMode = ViewMode.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of input and output names for the dynamic component
|
||||||
|
*/
|
||||||
|
protected inAndOutputNames: string[] = [
|
||||||
|
'objects',
|
||||||
|
'linkType',
|
||||||
|
'listID',
|
||||||
|
'showLabel',
|
||||||
|
'showThumbnails',
|
||||||
|
'context',
|
||||||
|
'viewMode',
|
||||||
|
'value',
|
||||||
|
'hideBadges',
|
||||||
|
'contentChange',
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the dynamic child component
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.instantiateComponent(this.objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever the inputs change, update the inputs of the dynamic component
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (hasNoValue(this.compRef)) {
|
||||||
|
// sometimes the component has not been initialized yet, so it first needs to be initialized
|
||||||
|
// before being called again
|
||||||
|
this.instantiateComponent(this.objects, changes);
|
||||||
|
} else {
|
||||||
|
// if an input or output has changed
|
||||||
|
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
|
||||||
|
this.connectInputsAndOutputs();
|
||||||
|
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
|
||||||
|
(this.compRef.instance as any).ngOnChanges(changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subs
|
||||||
|
.filter((subscription) => hasValue(subscription))
|
||||||
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
private instantiateComponent(objects: PaginatedList<ListableObject>, changes?: SimpleChanges): void {
|
||||||
|
// objects need to have same render type so we access just the first in the page
|
||||||
|
const component = this.getComponent(objects?.page[0]?.getRenderTypes(), this.viewMode, this.context);
|
||||||
|
|
||||||
|
const viewContainerRef = this.tabulatableObjectsDirective.viewContainerRef;
|
||||||
|
viewContainerRef.clear();
|
||||||
|
|
||||||
|
this.compRef = viewContainerRef.createComponent(
|
||||||
|
component, {
|
||||||
|
index: 0,
|
||||||
|
injector: undefined
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasValue(changes)) {
|
||||||
|
this.ngOnChanges(changes);
|
||||||
|
} else {
|
||||||
|
this.connectInputsAndOutputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.compRef.instance as any).reloadedObject) {
|
||||||
|
combineLatest([
|
||||||
|
observableOf(changes),
|
||||||
|
(this.compRef.instance as any).reloadedObject.pipe(take(1)) as Observable<PaginatedList<ListableObject>>,
|
||||||
|
]).subscribe(([simpleChanges, reloadedObjects]: [SimpleChanges, PaginatedList<ListableObject>]) => {
|
||||||
|
if (reloadedObjects) {
|
||||||
|
this.compRef.destroy();
|
||||||
|
this.objects = reloadedObjects;
|
||||||
|
this.instantiateComponent(reloadedObjects, simpleChanges);
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
this.contentChange.emit(reloadedObjects);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the component depending on the item's entity type, view mode and context
|
||||||
|
* @returns {GenericConstructor<Component>}
|
||||||
|
*/
|
||||||
|
getComponent(renderTypes: (string | GenericConstructor<ListableObject>)[],
|
||||||
|
viewMode: ViewMode,
|
||||||
|
context: Context): GenericConstructor<Component> {
|
||||||
|
return getTabulatableObjectsComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect the in and outputs of this component to the dynamic component,
|
||||||
|
* to ensure they're in sync
|
||||||
|
*/
|
||||||
|
protected connectInputsAndOutputs(): void {
|
||||||
|
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
|
||||||
|
this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
|
||||||
|
this.compRef.instance[name] = this[name];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
|
import { hasNoValue, hasValue } from '../../../empty.util';
|
||||||
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
|
import { ListableObject } from '../listable-object.model';
|
||||||
|
import {
|
||||||
|
DEFAULT_CONTEXT,
|
||||||
|
DEFAULT_THEME,
|
||||||
|
DEFAULT_VIEW_MODE, getMatch,
|
||||||
|
MatchRelevancy
|
||||||
|
} from '../listable-object/listable-object.decorator';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator used for rendering tabulatable objects
|
||||||
|
* @param objectType The object type or entity type the component represents
|
||||||
|
* @param viewMode The view mode the component represents
|
||||||
|
* @param context The optional context the component represents
|
||||||
|
* @param theme The optional theme for the component
|
||||||
|
*/
|
||||||
|
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<PaginatedList<ListableObject>>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
|
||||||
|
return function decorator(component: any) {
|
||||||
|
if (hasNoValue(objectsType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasNoValue(map.get(objectsType))) {
|
||||||
|
map.set(objectsType, new Map());
|
||||||
|
}
|
||||||
|
if (hasNoValue(map.get(objectsType).get(viewMode))) {
|
||||||
|
map.get(objectsType).set(viewMode, new Map());
|
||||||
|
}
|
||||||
|
if (hasNoValue(map.get(objectsType).get(viewMode).get(context))) {
|
||||||
|
map.get(objectsType).get(viewMode).set(context, new Map());
|
||||||
|
}
|
||||||
|
map.get(objectsType).get(viewMode).get(context).set(theme, component);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter to retrieve the matching tabulatable objects component
|
||||||
|
*
|
||||||
|
* Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
|
||||||
|
* The most relevant match between types is kept and eventually returned
|
||||||
|
*
|
||||||
|
* @param types The types of which one should match the tabulatable component
|
||||||
|
* @param viewMode The view mode that should match the components
|
||||||
|
* @param context The context that should match the components
|
||||||
|
* @param theme The theme that should match the components
|
||||||
|
*/
|
||||||
|
export function getTabulatableObjectsComponent(types: (string | GenericConstructor<ListableObject>)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
|
||||||
|
let currentBestMatch: MatchRelevancy = null;
|
||||||
|
for (const type of types) {
|
||||||
|
const typeMap = map.get(PaginatedList<typeof type>);
|
||||||
|
if (hasValue(typeMap)) {
|
||||||
|
const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
|
||||||
|
if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
|
||||||
|
currentBestMatch = match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasValue(currentBestMatch) ? currentBestMatch.match : null;
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
import { Directive, ViewContainerRef } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsTabulatableObjects]',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Directive used as a hook to know where to inject the dynamic listable object component
|
||||||
|
*/
|
||||||
|
export class TabulatableObjectsDirective {
|
||||||
|
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AbstractTabulatableElementComponent
|
||||||
|
} from '../../../object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
import { SearchResult } from '../../../search/models/search-result.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-result-list-element',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends SearchResult<any>> extends AbstractTabulatableElementComponent<T> {}
|
33
src/app/shared/object-table/object-table.component.html
Normal file
33
src/app/shared/object-table/object-table.component.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<ds-pagination
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[pageInfoState]="objects?.payload"
|
||||||
|
[collectionSize]="objects?.payload?.totalElements"
|
||||||
|
[sortOptions]="sortConfig"
|
||||||
|
[hideGear]="hideGear"
|
||||||
|
[objects]="objects"
|
||||||
|
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||||
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
|
[showPaginator]="showPaginator"
|
||||||
|
(pageChange)="onPageChange($event)"
|
||||||
|
(pageSizeChange)="onPageSizeChange($event)"
|
||||||
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
|
(paginationChange)="onPaginationChange($event)"
|
||||||
|
(prev)="goPrev()"
|
||||||
|
(next)="goNext()"
|
||||||
|
[retainScrollPosition]="true"
|
||||||
|
>
|
||||||
|
<div *ngIf="objects?.hasSucceeded">
|
||||||
|
<div @fadeIn>
|
||||||
|
<ds-tabulatable-objects-loader [objects]="objects.payload"
|
||||||
|
[context]="context"
|
||||||
|
[showThumbnails]="showThumbnails"
|
||||||
|
[linkType]="linkType">
|
||||||
|
</ds-tabulatable-objects-loader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="objects?.hasFailed" message="{{'error.objects' | translate}}"></ds-error>
|
||||||
|
<ds-themed-loading *ngIf="objects?.isLoading" message="{{'loading.objects' | translate}}"></ds-themed-loading>
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
|
23
src/app/shared/object-table/object-table.component.spec.ts
Normal file
23
src/app/shared/object-table/object-table.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ObjectTableComponent } from './object-table.component';
|
||||||
|
|
||||||
|
describe('ObjectTableComponent', () => {
|
||||||
|
let component: ObjectTableComponent;
|
||||||
|
let fixture: ComponentFixture<ObjectTableComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ObjectTableComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ObjectTableComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
201
src/app/shared/object-table/object-table.component.ts
Normal file
201
src/app/shared/object-table/object-table.component.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||||
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
|
||||||
|
import { Context } from '../../core/shared/context.model';
|
||||||
|
import { BehaviorSubject} from 'rxjs';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
|
import { fadeIn } from '../animations/fade';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
|
encapsulation: ViewEncapsulation.Emulated,
|
||||||
|
selector: 'ds-object-table',
|
||||||
|
templateUrl: './object-table.component.html',
|
||||||
|
styleUrls: ['./object-table.component.scss'],
|
||||||
|
animations: [fadeIn]
|
||||||
|
})
|
||||||
|
export class ObjectTableComponent {
|
||||||
|
/**
|
||||||
|
* The view mode of this component
|
||||||
|
*/
|
||||||
|
viewMode = ViewMode.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration
|
||||||
|
*/
|
||||||
|
@Input() config: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current sort configuration
|
||||||
|
*/
|
||||||
|
@Input() sortConfig: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
|
||||||
|
*/
|
||||||
|
@Input() showPaginator = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show the thumbnail preview
|
||||||
|
*/
|
||||||
|
@Input() showThumbnails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The whether or not the gear is hidden
|
||||||
|
*/
|
||||||
|
@Input() hideGear = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the pager is visible when there is only a single page of results
|
||||||
|
*/
|
||||||
|
@Input() hidePagerWhenSinglePage = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link type of the listable elements
|
||||||
|
*/
|
||||||
|
@Input() linkType: CollectionElementLinkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context of the listable elements
|
||||||
|
*/
|
||||||
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option for hiding the pagination detail
|
||||||
|
*/
|
||||||
|
@Input() hidePaginationDetail = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior subject to output the current listable objects
|
||||||
|
*/
|
||||||
|
private _objects$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter to make sure the observable is turned into an observable
|
||||||
|
* @param objects The new objects to output
|
||||||
|
*/
|
||||||
|
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
|
||||||
|
this._objects$.next(objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter to return the current objects
|
||||||
|
*/
|
||||||
|
get objects() {
|
||||||
|
return this._objects$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page is changed.
|
||||||
|
* Event's payload equals to the newly selected page.
|
||||||
|
*/
|
||||||
|
@Output() change: EventEmitter<{
|
||||||
|
pagination: PaginationComponentOptions,
|
||||||
|
sort: SortOptions
|
||||||
|
}> = new EventEmitter<{
|
||||||
|
pagination: PaginationComponentOptions,
|
||||||
|
sort: SortOptions
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page is changed.
|
||||||
|
* Event's payload equals to the newly selected page.
|
||||||
|
*/
|
||||||
|
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page wsize is changed.
|
||||||
|
* Event's payload equals to the newly selected page size.
|
||||||
|
*/
|
||||||
|
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort direction is changed.
|
||||||
|
* Event's payload equals to the newly selected sort direction.
|
||||||
|
*/
|
||||||
|
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when on of the pagination parameters changes
|
||||||
|
*/
|
||||||
|
@Output() paginationChange: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the sort field is changed.
|
||||||
|
* Event's payload equals to the newly selected sort field.
|
||||||
|
*/
|
||||||
|
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If showPaginator is set to true, emit when the previous button is clicked
|
||||||
|
*/
|
||||||
|
@Output() prev = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If showPaginator is set to true, emit when the next button is clicked
|
||||||
|
*/
|
||||||
|
@Output() next = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
data: any = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._objects$ = new BehaviorSubject(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the current page when it changes
|
||||||
|
* @param event The new page
|
||||||
|
*/
|
||||||
|
onPageChange(event) {
|
||||||
|
this.pageChange.emit(event);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Emits the current page size when it changes
|
||||||
|
* @param event The new page size
|
||||||
|
*/
|
||||||
|
onPageSizeChange(event) {
|
||||||
|
this.pageSizeChange.emit(event);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Emits the current sort direction when it changes
|
||||||
|
* @param event The new sort direction
|
||||||
|
*/
|
||||||
|
onSortDirectionChange(event) {
|
||||||
|
this.sortDirectionChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the current sort field when it changes
|
||||||
|
* @param event The new sort field
|
||||||
|
*/
|
||||||
|
onSortFieldChange(event) {
|
||||||
|
this.sortFieldChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the current pagination when it changes
|
||||||
|
* @param event The new pagination
|
||||||
|
*/
|
||||||
|
onPaginationChange(event) {
|
||||||
|
this.paginationChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the previous page
|
||||||
|
*/
|
||||||
|
goPrev() {
|
||||||
|
this.prev.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to the next page
|
||||||
|
*/
|
||||||
|
goNext() {
|
||||||
|
this.next.emit(true);
|
||||||
|
}
|
||||||
|
}
|
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="search-content" class="col-12">
|
<div id="search-content" class="col-12">
|
||||||
<div class="d-block d-md-none search-controls clearfix">
|
<div class="d-block d-md-none search-controls clearfix">
|
||||||
<ds-view-mode-switch [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
<ds-view-mode-switch *ngIf="showViewModes" [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||||
|
@@ -285,6 +285,18 @@ import { NgxPaginationModule } from 'ngx-pagination';
|
|||||||
import { SplitPipe } from './utils/split.pipe';
|
import { SplitPipe } from './utils/split.pipe';
|
||||||
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
|
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
|
||||||
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
|
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
|
||||||
|
import { NotificationBoxComponent } from './notification-box/notification-box.component';
|
||||||
|
import { ObjectTableComponent } from './object-table/object-table.component';
|
||||||
|
import { TabulatableObjectsLoaderComponent } from './object-collection/shared/tabulatable-objects/tabulatable-objects-loader.component';
|
||||||
|
import {
|
||||||
|
TabulatableObjectsDirective
|
||||||
|
} from './object-collection/shared/tabulatable-objects/tabulatable-objects.directive';
|
||||||
|
import {
|
||||||
|
AbstractTabulatableElementComponent
|
||||||
|
} from './object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
|
||||||
|
import {
|
||||||
|
TabulatableResultListElementsComponent
|
||||||
|
} from './object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -348,7 +360,9 @@ const COMPONENTS = [
|
|||||||
ThemedObjectListComponent,
|
ThemedObjectListComponent,
|
||||||
ObjectDetailComponent,
|
ObjectDetailComponent,
|
||||||
ObjectGridComponent,
|
ObjectGridComponent,
|
||||||
|
ObjectTableComponent,
|
||||||
AbstractListableElementComponent,
|
AbstractListableElementComponent,
|
||||||
|
AbstractTabulatableElementComponent,
|
||||||
ObjectCollectionComponent,
|
ObjectCollectionComponent,
|
||||||
PaginationComponent,
|
PaginationComponent,
|
||||||
RSSComponent,
|
RSSComponent,
|
||||||
@@ -414,6 +428,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
CollectionListElementComponent,
|
CollectionListElementComponent,
|
||||||
CommunityListElementComponent,
|
CommunityListElementComponent,
|
||||||
SearchResultListElementComponent,
|
SearchResultListElementComponent,
|
||||||
|
TabulatableResultListElementsComponent,
|
||||||
CommunitySearchResultListElementComponent,
|
CommunitySearchResultListElementComponent,
|
||||||
CollectionSearchResultListElementComponent,
|
CollectionSearchResultListElementComponent,
|
||||||
CollectionGridElementComponent,
|
CollectionGridElementComponent,
|
||||||
@@ -469,7 +484,9 @@ const ENTRY_COMPONENTS = [
|
|||||||
AdvancedClaimedTaskActionRatingComponent,
|
AdvancedClaimedTaskActionRatingComponent,
|
||||||
EpersonGroupListComponent,
|
EpersonGroupListComponent,
|
||||||
EpersonSearchBoxComponent,
|
EpersonSearchBoxComponent,
|
||||||
GroupSearchBoxComponent
|
GroupSearchBoxComponent,
|
||||||
|
NotificationBoxComponent,
|
||||||
|
TabulatableObjectsLoaderComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
@@ -488,6 +505,7 @@ const DIRECTIVES = [
|
|||||||
RoleDirective,
|
RoleDirective,
|
||||||
MetadataRepresentationDirective,
|
MetadataRepresentationDirective,
|
||||||
ListableObjectDirective,
|
ListableObjectDirective,
|
||||||
|
TabulatableObjectsDirective,
|
||||||
ClaimedTaskActionsDirective,
|
ClaimedTaskActionsDirective,
|
||||||
FileValueAccessorDirective,
|
FileValueAccessorDirective,
|
||||||
FileValidator,
|
FileValidator,
|
||||||
|
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
"admin.notifications.recitersuggestion.page.title": "Suggestions",
|
"admin.notifications.recitersuggestion.page.title": "Suggestions",
|
||||||
|
|
||||||
|
"admin.notify.dashboard": "Dashboard",
|
||||||
|
|
||||||
"error-page.description.401": "unauthorized",
|
"error-page.description.401": "unauthorized",
|
||||||
|
|
||||||
"error-page.description.403": "forbidden",
|
"error-page.description.403": "forbidden",
|
||||||
@@ -3075,6 +3077,10 @@
|
|||||||
|
|
||||||
"menu.section.icon.export": "Export menu section",
|
"menu.section.icon.export": "Export menu section",
|
||||||
|
|
||||||
|
"menu.section.notify_dashboard": "Dashboard",
|
||||||
|
|
||||||
|
"menu.section.coar_notify": "COAR Notify",
|
||||||
|
|
||||||
"menu.section.icon.find": "Find menu section",
|
"menu.section.icon.find": "Find menu section",
|
||||||
|
|
||||||
"menu.section.icon.health": "Health check menu section",
|
"menu.section.icon.health": "Health check menu section",
|
||||||
@@ -3437,7 +3443,199 @@
|
|||||||
|
|
||||||
"quality-assurance.event.reason": "Reason",
|
"quality-assurance.event.reason": "Reason",
|
||||||
|
|
||||||
"orgunit.listelement.badge": "Organizational Unit",
|
"admin-notify-dashboard.title": "Notify Dashboard",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.metrics": "Metrics",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.received-ldn": "Number of received LDN",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.generated-ldn": "Number of generated LDN",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.accepted": "Accepted",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.processed": "Processed LDN",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.failure": "Failure",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.untrusted": "Untrusted",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.delivered": "Delivered",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.queued": "Queued",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.queued-for-retry": "Queued for retry",
|
||||||
|
|
||||||
|
"admin-notify-dashboard.involved-items": "Involved items",
|
||||||
|
|
||||||
|
"admin.notify.dashboard.breadcrumbs": "Dashboard",
|
||||||
|
|
||||||
|
"admin.notify.dashboard.inbound": "Inbound messages",
|
||||||
|
|
||||||
|
"admin.notify.dashboard.inbound-logs": "Logs/Inbound",
|
||||||
|
|
||||||
|
"admin.notify.dashboard.outbound": "Outbound messages",
|
||||||
|
|
||||||
|
"admin.notify.dashboard.outbound-logs": "Logs/Outbound",
|
||||||
|
|
||||||
|
"NOTIFY.incoming.search.results.head": "Incoming",
|
||||||
|
|
||||||
|
"search.filters.filter.relateditem.head": "Related item",
|
||||||
|
|
||||||
|
"search.filters.filter.origin.head": "Origin",
|
||||||
|
|
||||||
|
"search.filters.filter.target.head": "Target",
|
||||||
|
|
||||||
|
"search.filters.filter.queue_status.head": "Queue status",
|
||||||
|
|
||||||
|
"search.filters.filter.activity_stream_type.head": "Activity stream type",
|
||||||
|
|
||||||
|
"search.filters.filter.coar_notify_type.head": "COAR Notify type",
|
||||||
|
|
||||||
|
"search.filters.filter.notification_type.head": "Notification type",
|
||||||
|
|
||||||
|
"search.filters.filter.relateditem.label": "Search related items",
|
||||||
|
|
||||||
|
"search.filters.filter.queue_status.label": "Search queue status",
|
||||||
|
|
||||||
|
"search.filters.filter.target.label": "Search target",
|
||||||
|
|
||||||
|
"search.filters.filter.activity_stream_type.label": "Search activity stream type",
|
||||||
|
|
||||||
|
"search.filters.filter.coar_notify_type.label": "Search COAR Notify type",
|
||||||
|
|
||||||
|
"search.filters.filter.notification_type.label": "Search notification type",
|
||||||
|
|
||||||
|
"search.filters.filter.relateditem.placeholder": "Related items",
|
||||||
|
|
||||||
|
"search.filters.filter.target.placeholder": "Target",
|
||||||
|
|
||||||
|
"search.filters.filter.origin.label": "Search source",
|
||||||
|
|
||||||
|
"search.filters.filter.origin.placeholder": "Source",
|
||||||
|
|
||||||
|
"search.filters.filter.queue_status.placeholder": "Queue status",
|
||||||
|
|
||||||
|
"search.filters.filter.activity_stream_type.placeholder": "Activity stream type",
|
||||||
|
|
||||||
|
"search.filters.filter.coar_notify_type.placeholder": "COAR Notify type",
|
||||||
|
|
||||||
|
"search.filters.filter.notification_type.placeholder": "Notification",
|
||||||
|
|
||||||
|
"search.filters.coar_notify_type.coar-notify:ReviewAction": "Review action",
|
||||||
|
|
||||||
|
"notify-detail-modal.coar-notify:ReviewAction": "Review action",
|
||||||
|
|
||||||
|
"search.filters.coar_notify_type.coar-notify:EndorsementAction": "Endorsement action",
|
||||||
|
|
||||||
|
"notify-detail-modal.coar-notify:EndorsementAction": "Endorsement action",
|
||||||
|
|
||||||
|
"search.filters.coar_notify_type.coar-notify:IngestAction": "Ingest action",
|
||||||
|
|
||||||
|
"notify-detail-modal.coar-notify:IngestAction": "Ingest action",
|
||||||
|
|
||||||
|
"search.filters.coar_notify_type.coar-notify:RelationshipAction": "Relationship action",
|
||||||
|
|
||||||
|
"notify-detail-modal.coar-notify:RelationshipAction": "Relationship action",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_QUEUED": "Queued",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_QUEUED": "Queued",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_QUEUED_FOR_RETRY": "Queued for retry",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_QUEUED_FOR_RETRY": "Queued for retry",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_PROCESSING": "Processing",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_PROCESSING": "Processing",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_PROCESSED": "Processed",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_PROCESSED": "Processed",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_FAILED": "Failed",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_FAILED": "Failed",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_UNTRUSTED": "Untrusted",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_UNTRUSTED": "Untrusted",
|
||||||
|
|
||||||
|
"search.filters.queue_status.QUEUE_STATUS_UNMAPPED_ACTION": "Unmapped Action",
|
||||||
|
|
||||||
|
"notify-detail-modal.QUEUE_STATUS_UNMAPPED_ACTION": "Unmapped Action",
|
||||||
|
|
||||||
|
"sorting.queue_last_start_time.DESC": "Last started queue Descending",
|
||||||
|
|
||||||
|
"sorting.queue_last_start_time.ASC": "Last started queue Ascending",
|
||||||
|
|
||||||
|
"sorting.queue_attempts.DESC": "Queue attempted Descending",
|
||||||
|
|
||||||
|
"sorting.queue_attempts.ASC": "Queue attempted Ascending",
|
||||||
|
|
||||||
|
"type.notify-detail-modal": "Type",
|
||||||
|
|
||||||
|
"id.notify-detail-modal": "Id",
|
||||||
|
|
||||||
|
"coarNotifyType.notify-detail-modal": "COAR Notify type",
|
||||||
|
|
||||||
|
"activityStreamType.notify-detail-modal": "Activity stream type",
|
||||||
|
|
||||||
|
"inReplyTo.notify-detail-modal": "In reply to",
|
||||||
|
|
||||||
|
"object.notify-detail-modal": "Repository Item",
|
||||||
|
|
||||||
|
"context.notify-detail-modal": "Repository Item",
|
||||||
|
|
||||||
|
"queueAttempts.notify-detail-modal": "Queue attempts",
|
||||||
|
|
||||||
|
"queueLastStartTime.notify-detail-modal": "Queue last started",
|
||||||
|
|
||||||
|
"origin.notify-detail-modal": "LDN Service",
|
||||||
|
|
||||||
|
"target.notify-detail-modal": "LDN Service",
|
||||||
|
|
||||||
|
"queueStatusLabel.notify-detail-modal": "Queue status",
|
||||||
|
|
||||||
|
"queueTimeout.notify-detail-modal": "Queue timeout",
|
||||||
|
|
||||||
|
"notify-message-modal.title": "Message Detail",
|
||||||
|
|
||||||
|
"notify-message-result.timestamp": "Timestamp",
|
||||||
|
|
||||||
|
"notify-message-result.repositoryItem": "Repository Item",
|
||||||
|
|
||||||
|
"notify-message-result.ldnService": "LDN Service",
|
||||||
|
|
||||||
|
"notify-message-result.type": "Type",
|
||||||
|
|
||||||
|
"notify-message-result.status": "Status",
|
||||||
|
|
||||||
|
"notify-message-result.action": "Action",
|
||||||
|
|
||||||
|
"notify-message-result.detail": "Detail",
|
||||||
|
|
||||||
|
"notify-message-result.reprocess": "Reprocess",
|
||||||
|
|
||||||
|
"notify-queue-status.processed": "Processed",
|
||||||
|
|
||||||
|
"notify-queue-status.failed": "Failed",
|
||||||
|
|
||||||
|
"notify-queue-status.queue_retry": "Queued for retry",
|
||||||
|
|
||||||
|
"notify-queue-status.unmapped_action": "Unmapped action",
|
||||||
|
|
||||||
|
"notify-queue-status.processing": "Processing",
|
||||||
|
|
||||||
|
"notify-queue-status.queued": "Queued",
|
||||||
|
|
||||||
|
"notify-queue-status.untrusted": "Untrusted",
|
||||||
|
|
||||||
|
"ldnService.notify-detail-modal": "LDN Service",
|
||||||
|
|
||||||
|
"relatedItem.notify-detail-modal": "Related Item",
|
||||||
|
|
||||||
|
"orgunit.listelement.badge": "Repository Item",
|
||||||
|
|
||||||
"orgunit.listelement.no-title": "Untitled",
|
"orgunit.listelement.no-title": "Untitled",
|
||||||
|
|
||||||
|
@@ -23,6 +23,9 @@ import { HomeConfig } from './homepage-config.interface';
|
|||||||
import { MarkdownConfig } from './markdown-config.interface';
|
import { MarkdownConfig } from './markdown-config.interface';
|
||||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||||
|
import {
|
||||||
|
AdminNotifyMetricsRow
|
||||||
|
} from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||||
|
|
||||||
interface AppConfig extends Config {
|
interface AppConfig extends Config {
|
||||||
ui: UIServerConfig;
|
ui: UIServerConfig;
|
||||||
@@ -50,6 +53,7 @@ interface AppConfig extends Config {
|
|||||||
markdown: MarkdownConfig;
|
markdown: MarkdownConfig;
|
||||||
vocabularies: FilterVocabularyConfig[];
|
vocabularies: FilterVocabularyConfig[];
|
||||||
comcolSelectionSort: DiscoverySortConfig;
|
comcolSelectionSort: DiscoverySortConfig;
|
||||||
|
notifyMetrics: AdminNotifyMetricsRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -23,6 +23,9 @@ import { HomeConfig } from './homepage-config.interface';
|
|||||||
import { MarkdownConfig } from './markdown-config.interface';
|
import { MarkdownConfig } from './markdown-config.interface';
|
||||||
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
||||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||||
|
import {
|
||||||
|
AdminNotifyMetricsRow
|
||||||
|
} from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
|
||||||
|
|
||||||
export class DefaultAppConfig implements AppConfig {
|
export class DefaultAppConfig implements AppConfig {
|
||||||
production = false;
|
production = false;
|
||||||
@@ -443,4 +446,69 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
sortField:'dc.title',
|
sortField:'dc.title',
|
||||||
sortDirection:'ASC',
|
sortDirection:'ASC',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
notifyMetrics: AdminNotifyMetricsRow[] = [
|
||||||
|
{
|
||||||
|
title: 'admin-notify-dashboard.received-ldn',
|
||||||
|
boxes: [
|
||||||
|
{
|
||||||
|
color: '#B8DAFF',
|
||||||
|
title: 'admin-notify-dashboard.accepted',
|
||||||
|
config: 'NOTIFY.incoming.accepted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#D4EDDA',
|
||||||
|
title: 'admin-notify-dashboard.processed',
|
||||||
|
config: 'NOTIFY.incoming.processed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.failure',
|
||||||
|
config: 'NOTIFY.incoming.failure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.untrusted',
|
||||||
|
config: 'NOTIFY.incoming.untrusted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#43515F',
|
||||||
|
title: 'admin-notify-dashboard.involved-items',
|
||||||
|
textColor: '#fff',
|
||||||
|
config: 'NOTIFY.incoming.involvedItems',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'admin-notify-dashboard.generated-ldn',
|
||||||
|
boxes: [
|
||||||
|
{
|
||||||
|
color: '#D4EDDA',
|
||||||
|
title: 'admin-notify-dashboard.delivered',
|
||||||
|
config: 'NOTIFY.outgoing.delivered'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#B8DAFF',
|
||||||
|
title: 'admin-notify-dashboard.queued',
|
||||||
|
config: 'NOTIFY.outgoing.queued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDEEBB',
|
||||||
|
title: 'admin-notify-dashboard.queued-for-retry',
|
||||||
|
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.failure',
|
||||||
|
config: 'NOTIFY.outgoing.failure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#43515F',
|
||||||
|
title: 'admin-notify-dashboard.involved-items',
|
||||||
|
textColor: '#fff',
|
||||||
|
config: 'NOTIFY.outgoing.involvedItems',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@@ -317,5 +317,70 @@ export const environment: BuildConfig = {
|
|||||||
vocabulary: 'srsc',
|
vocabulary: 'srsc',
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
notifyMetrics: [
|
||||||
|
{
|
||||||
|
title: 'admin-notify-dashboard.received-ldn',
|
||||||
|
boxes: [
|
||||||
|
{
|
||||||
|
color: '#B8DAFF',
|
||||||
|
title: 'admin-notify-dashboard.accepted',
|
||||||
|
config: 'NOTIFY.incoming.accepted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#D4EDDA',
|
||||||
|
title: 'admin-notify-dashboard.processed',
|
||||||
|
config: 'NOTIFY.incoming.processed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.failure',
|
||||||
|
config: 'NOTIFY.incoming.failure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.untrusted',
|
||||||
|
config: 'NOTIFY.incoming.untrusted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#43515F',
|
||||||
|
title: 'admin-notify-dashboard.involved-items',
|
||||||
|
textColor: '#fff',
|
||||||
|
config: 'NOTIFY.incoming.involvedItems',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'admin-notify-dashboard.generated-ldn',
|
||||||
|
boxes: [
|
||||||
|
{
|
||||||
|
color: '#D4EDDA',
|
||||||
|
title: 'admin-notify-dashboard.delivered',
|
||||||
|
config: 'NOTIFY.outgoing.delivered'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#B8DAFF',
|
||||||
|
title: 'admin-notify-dashboard.queued',
|
||||||
|
config: 'NOTIFY.outgoing.queued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDEEBB',
|
||||||
|
title: 'admin-notify-dashboard.queued-for-retry',
|
||||||
|
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#FDBBC7',
|
||||||
|
title: 'admin-notify-dashboard.failure',
|
||||||
|
config: 'NOTIFY.outgoing.failure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#43515F',
|
||||||
|
title: 'admin-notify-dashboard.involved-items',
|
||||||
|
textColor: '#fff',
|
||||||
|
config: 'NOTIFY.outgoing.involvedItems',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user