diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts index 43dc4f3462..282ca430a2 100644 --- a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts +++ b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts @@ -1,34 +1,34 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { I18nBreadcrumbResolver } from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver'; -import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; -import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; -import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.component'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {I18nBreadcrumbResolver} from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver'; +import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component'; +import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component'; +import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component'; @NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - pathMatch: 'full', - component: LdnServicesOverviewComponent, - resolve: {breadcrumb: I18nBreadcrumbResolver}, - data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'}, - }, - { - path: 'new', - resolve: {breadcrumb: I18nBreadcrumbResolver}, - component: LdnServiceNewComponent, - data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'} - }, - { - path: 'edit/:serviceId', - resolve: {breadcrumb: I18nBreadcrumbResolver}, - component: LdnServiceFormEditComponent, - data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'} - }, - ]), - ] + imports: [ + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: LdnServicesOverviewComponent, + resolve: {breadcrumb: I18nBreadcrumbResolver}, + data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'}, + }, + { + path: 'new', + resolve: {breadcrumb: I18nBreadcrumbResolver}, + component: LdnServiceNewComponent, + data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'} + }, + { + path: 'edit/:serviceId', + resolve: {breadcrumb: I18nBreadcrumbResolver}, + component: LdnServiceFormEditComponent, + data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'} + }, + ]), + ] }) export class AdminLdnServicesRoutingModule { diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts index 9ca3775a9e..48ad89fdcc 100644 --- a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts +++ b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts @@ -1,29 +1,29 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module'; -import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; -import { SharedModule } from '../../shared/shared.module'; -import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; -import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; -import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.component'; -import { FormsModule } from '@angular/forms'; -import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {AdminLdnServicesRoutingModule} from './admin-ldn-services-routing.module'; +import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component'; +import {SharedModule} from '../../shared/shared.module'; +import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component'; +import {LdnServiceFormComponent} from './ldn-service-form/ldn-service-form.component'; +import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component'; +import {FormsModule} from '@angular/forms'; +import {LdnItemfiltersService} from './ldn-services-data/ldn-itemfilters-data.service'; @NgModule({ - imports: [ - CommonModule, - SharedModule, - AdminLdnServicesRoutingModule, - FormsModule - ], - declarations: [ - LdnServicesOverviewComponent, - LdnServiceNewComponent, - LdnServiceFormComponent, - LdnServiceFormEditComponent, - ], - providers: [LdnItemfiltersService] + imports: [ + CommonModule, + SharedModule, + AdminLdnServicesRoutingModule, + FormsModule + ], + declarations: [ + LdnServicesOverviewComponent, + LdnServiceNewComponent, + LdnServiceFormComponent, + LdnServiceFormEditComponent, + ], + providers: [LdnItemfiltersService] }) export class AdminLdnServicesModule { } diff --git a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.html b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.html index 368b312cd1..1438cb6ce4 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.html +++ b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.html @@ -1,281 +1,412 @@
-
-
-

{{ 'ldn-edit-registered-service.title' | translate }}

+ +
+

{{ 'ldn-edit-registered-service.title' | translate }}

+
+ +
+ +
+ +
+
- -
- -
- -
-
+
+
+ +
+ + +
+ {{ 'ldn-new-service.form.error.name' | translate }} +
+
+ + +
+ + +
+ + +
+ + +
+ {{ 'ldn-new-service.form.error.url' | translate }} +
+
+ + +
+ + +
+ {{ 'ldn-new-service.form.error.ldnurl' | translate }} +
+
+ + +
+ + +
+ {{ 'ldn-new-service.form.error.score' | translate }} +
+
+ + +
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ + +
+ + + + +
+
+
+
+ + + +
-
+
-
- - -
- -
-   -
- - -
- - -
- -
-   -
- - -
- - -
- -
-   -
- - -
- - -
- -
-   -
- -
-
- -
-
- -
-
- -
-
-
-
- -
- - - - -
-
- -
- -
- - - -
- -
- -
-
-
-
- - -
- - +
+ +
+
+ + +
- +
+
+ +
+ +
+
+
+
+ + +
+
+ + + + +
+
+ +
- {{ 'ldn-new-service.form.label.addPattern' | translate }} + {{ 'ldn-new-service.form.label.addPattern' | translate }} -
-   + +
+
+ +
+ +
+
+
+
+ +
+
+
+
-
-
- -
-
- -
-
- -
-
-
-
+ +
-
+ - - - -
-
- -
-
- - - -
- -
- -
-
-
-
- -
- - -
+
+
+
+
+ + + - -
- - {{ 'ldn-new-service.form.label.addPattern' | translate }} - - -
-   -
- +
+
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ + +
+
- + +
+ + + + + +
+ -
- - - - + -
+
- - - + + + +
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.scss b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.scss index b686e6533b..91666cde50 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.scss +++ b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.scss @@ -1,8 +1,9 @@ +@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss'; +@import '../../../shared/form/form.component.scss'; + form { - max-width: 800px; font-size: 14px; position: relative; - } input[type="text"], @@ -30,7 +31,6 @@ textarea { .add-pattern-link { color: #0048ff; cursor: pointer; - margin-left: 10px; } .remove-pattern-link { @@ -49,6 +49,12 @@ textarea { color: #000000; } +.error-text { + color: red; + font-size: 0.8em; + margin-top: 5px; +} + .toggle-switch { display: flex; align-items: center; @@ -95,6 +101,11 @@ textarea { margin-top: 10px; } +.small-text { + font-size: 0.7em; + color: #888; +} + .toggle-switch { cursor: pointer; margin-top: 3px; @@ -113,11 +124,6 @@ textarea { margin-left: 5px; } -form button.btn.btn-primary[type="submit"] { - position: absolute; - right: 8px; -} - .submission-form-footer { border-radius: var(--bs-card-border-radius); bottom: 0; @@ -127,7 +133,7 @@ form button.btn.btn-primary[type="submit"] { } .marked-for-deletion { - background-color: #ffcccc; + background-color: lighten($red, 30%); } diff --git a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.spec.ts index 332d43cae3..2fa27ac743 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.spec.ts @@ -1,23 +1,80 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { LdnServiceFormEditComponent } from './ldn-service-form-edit.component'; +import {NgbDropdownModule, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {LdnServiceFormEditComponent} from './ldn-service-form-edit.component'; +import {ChangeDetectorRef, EventEmitter} from '@angular/core'; +import {FormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {ActivatedRoute, Router} from '@angular/router'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {PaginationService} from 'ngx-pagination'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service'; +import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service'; +import {RouterStub} from '../../../shared/testing/router.stub'; +import {MockActivatedRoute} from '../../../shared/mocks/active-router.mock'; +import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub'; +import {of} from 'rxjs'; +import {RouteService} from '../../../core/services/route.service'; +import {provideMockStore} from '@ngrx/store/testing'; describe('LdnServiceFormEditComponent', () => { - let component: LdnServiceFormEditComponent; - let fixture: ComponentFixture; + let component: LdnServiceFormEditComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [LdnServiceFormEditComponent] - }) - .compileComponents(); + let ldnServicesService: any; + let ldnItemfiltersService: any; + let cdRefStub: any; + let modalService: any; - fixture = TestBed.createComponent(LdnServiceFormEditComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + const translateServiceStub = { + get: () => of('translated-text'), + instant: () => 'translated-text', + onLangChange: new EventEmitter(), + onTranslationChange: new EventEmitter(), + onDefaultLangChange: new EventEmitter() + }; + + beforeEach(async () => { + ldnServicesService = { + update: () => ({}), + }; + ldnItemfiltersService = { + findAll: () => of(['item1', 'item2']), + }; + cdRefStub = Object.assign({ + detectChanges: () => fixture.detectChanges() }); + modalService = { + open: () => {/*comment*/ + } + }; - it('should create', () => { - expect(component).toBeTruthy(); - }); + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule], + declarations: [LdnServiceFormEditComponent], + providers: [ + {provide: LdnServicesService, useValue: ldnServicesService}, + {provide: LdnItemfiltersService, useValue: ldnItemfiltersService}, + {provide: Router, useValue: new RouterStub()}, + {provide: ActivatedRoute, useValue: new MockActivatedRoute()}, + {provide: ChangeDetectorRef, useValue: cdRefStub}, + {provide: NgbModal, useValue: modalService}, + {provide: NotificationsService, useValue: NotificationsServiceStub}, + {provide: TranslateService, useValue: translateServiceStub}, + {provide: PaginationService, useValue: {}}, + FormBuilder, + RouteService, + provideMockStore({}), + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LdnServiceFormEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.ts index fa460afbca..842118b774 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form-edit/ldn-service-form-edit.component.ts @@ -1,409 +1,579 @@ -import { ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; -import { ActivatedRoute, Router } from '@angular/router'; -import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; -import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patterns'; -import { animate, state, style, transition, trigger } from '@angular/animations'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { LdnService } from '../ldn-services-model/ldn-services.model'; -import { RemoteData } from 'src/app/core/data/remote-data'; -import { Operation } from 'fast-json-patch'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { LdnItemfiltersService } from '../ldn-services-data/ldn-itemfilters-data.service'; -import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { Observable } from 'rxjs'; -import { PaginationService } from '../../../core/pagination/pagination.service'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import {ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type'; +import {ActivatedRoute, Router} from '@angular/router'; +import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service'; +import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns'; +import {animate, state, style, transition, trigger} from '@angular/animations'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {TranslateService} from '@ngx-translate/core'; +import {LdnService} from '../ldn-services-model/ldn-services.model'; +import {RemoteData} from 'src/app/core/data/remote-data'; +import {Operation} from 'fast-json-patch'; +import {getFirstCompletedRemoteData} from '../../../core/shared/operators'; +import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service'; +import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {Observable} from 'rxjs'; +import {PaginationService} from '../../../core/pagination/pagination.service'; +import {FindListOptions} from '../../../core/data/find-list-options.model'; +import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model'; +import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model'; + +/** + * Component for editing LDN service through a form that allows to edit the properties of the selected service + */ @Component({ - selector: 'ds-ldn-service-form-edit', - templateUrl: './ldn-service-form-edit.component.html', - styleUrls: ['./ldn-service-form-edit.component.scss'], - animations: [ - trigger('toggleAnimation', [ - state('true', style({})), - state('false', style({})), - transition('true <=> false', animate('300ms ease-in')), - ]), - ], + selector: 'ds-ldn-service-form-edit', + templateUrl: './ldn-service-form-edit.component.html', + styleUrls: ['./ldn-service-form-edit.component.scss'], + animations: [ + trigger('toggleAnimation', [ + state('true', style({})), + state('false', style({})), + transition('true <=> false', animate('300ms ease-in')), + ]), + ], }) export class LdnServiceFormEditComponent implements OnInit { - formModel: FormGroup; - @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef; - @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef; + formModel: FormGroup; + @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef; + @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef; - public inboundPatterns: object[] = notifyPatterns; - public outboundPatterns: object[] = notifyPatterns; - itemfiltersRD$: Observable>>; - config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 20 + public inboundPatterns: string[] = notifyPatterns; + public outboundPatterns: string[] = notifyPatterns; + itemfiltersRD$: Observable>>; + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 20 + }); + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'po', + pageSize: 20 + }); + @Input() public name: string; + @Input() public description: string; + @Input() public url: string; + @Input() public ldnUrl: string; + @Input() public score: number; + @Input() public inboundPattern: string; + @Input() public outboundPattern: string; + @Input() public constraint: string; + @Input() public automatic: boolean; + @Input() public headerKey: string; + markedForDeletionInboundPattern: number[] = []; + markedForDeletionOutboundPattern: number[] = []; + selectedOutboundPatterns: string[]; + selectedInboundItemfilters: string[]; + selectedOutboundItemfilters: string[]; + selectedInboundPatterns: string[]; + protected serviceId: string; + private deletedInboundPatterns: number[] = []; + private deletedOutboundPatterns: number[] = []; + private modalRef: any; + private service: LdnService; + private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select'; + + constructor( + protected ldnServicesService: LdnServicesService, + private ldnItemfiltersService: LdnItemfiltersService, + private formBuilder: FormBuilder, + private router: Router, + private route: ActivatedRoute, + private cdRef: ChangeDetectorRef, + protected modalService: NgbModal, + private notificationService: NotificationsService, + private translateService: TranslateService, + protected paginationService: PaginationService + ) { + + this.formModel = this.formBuilder.group({ + id: [''], + name: ['', Validators.required], + description: ['', Validators.required], + url: ['', Validators.required], + ldnUrl: ['', Validators.required], + score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''], + outboundPattern: [''], + constraintPattern: [''], + enabled: [''], + notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), + notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]), + type: LDN_SERVICE.value, }); - pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'po', - pageSize: 20 + } + + ngOnInit(): void { + this.route.params.subscribe((params) => { + this.serviceId = params.serviceId; + if (this.serviceId) { + this.fetchServiceData(this.serviceId); + } }); - @Input() public name: string; - @Input() public description: string; - @Input() public url: string; - @Input() public ldnUrl: string; - @Input() public inboundPattern: string; - @Input() public outboundPattern: string; - @Input() public constraint: string; - @Input() public automatic: boolean; - @Input() public headerKey: string; - markedForDeletionInboundPattern: number[] = []; - markedForDeletionOutboundPattern: number[] = []; - protected serviceId: string; - private originalInboundPatterns: any[] = []; - private originalOutboundPatterns: any[] = []; - private deletedInboundPatterns: number[] = []; - private deletedOutboundPatterns: number[] = []; - private modalRef: any; - private service: LdnService; + this.setItemfilters(); + } - constructor( - protected ldnServicesService: LdnServicesService, - private ldnItemfiltersService: LdnItemfiltersService, - private formBuilder: FormBuilder, - private router: Router, - private route: ActivatedRoute, - private cdRef: ChangeDetectorRef, - protected modalService: NgbModal, - private notificationService: NotificationsService, - private translateService: TranslateService, - protected paginationService: PaginationService - ) { + /** + * Sets item filters using LDN item filters service + */ + setItemfilters() { + this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe( + getFirstCompletedRemoteData()); + } - this.formModel = this.formBuilder.group({ - id: [''], - name: ['', Validators.required], - description: ['', Validators.required], - url: ['', Validators.required], - ldnUrl: ['', Validators.required], - inboundPattern: [''], - outboundPattern: [''], - constraintPattern: [''], - enabled: [''], - notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), - notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]), - type: LDN_SERVICE.value, - }); - } + /** + * Fetches LDN service data by ID and updates the form + * @param serviceId - The ID of the LDN service + */ + fetchServiceData(serviceId: string): void { + this.ldnServicesService.findById(serviceId).pipe( + getFirstCompletedRemoteData() + ).subscribe( + (data: RemoteData) => { + if (data.hasSucceeded) { + this.service = data.payload; - ngOnInit(): void { - this.route.params.subscribe((params) => { - this.serviceId = params.serviceId; - if (this.serviceId) { - this.fetchServiceData(this.serviceId); - } - }); - this.setItemfilters(); - } - - setItemfilters() { - this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe( - getFirstCompletedRemoteData()); - } - - - fetchServiceData(serviceId: string): void { - this.ldnServicesService.findById(serviceId).pipe( - getFirstCompletedRemoteData() - ).subscribe( - (data: RemoteData) => { - if (data.hasSucceeded) { - this.service = data.payload; - - this.formModel.patchValue({ - id: this.service.id, - name: this.service.name, - description: this.service.description, - url: this.service.url, - ldnUrl: this.service.ldnUrl, - type: this.service.type, - enabled: this.service.enabled - }); - - const inboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; - inboundPatternsArray.clear(); - - this.service.notifyServiceInboundPatterns.forEach((pattern: any) => { - const patternFormGroup = this.initializeInboundPatternFormGroup(); - patternFormGroup.patchValue(pattern); - inboundPatternsArray.push(patternFormGroup); - this.cdRef.detectChanges(); - }); - - const outboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; - outboundPatternsArray.clear(); - - this.service.notifyServiceOutboundPatterns.forEach((pattern: any) => { - const patternFormGroup = this.initializeOutboundPatternFormGroup(); - patternFormGroup.patchValue(pattern); - outboundPatternsArray.push(patternFormGroup); - - this.cdRef.detectChanges(); - }); - this.originalInboundPatterns = [...this.service.notifyServiceInboundPatterns]; - this.originalOutboundPatterns = [...this.service.notifyServiceOutboundPatterns]; - } - }, - ); - } - - generatePatchOperations(): any[] { - const patchOperations: any[] = []; - - this.createReplaceOperation(patchOperations, 'name', '/name'); - this.createReplaceOperation(patchOperations, 'description', '/description'); - this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl'); - this.createReplaceOperation(patchOperations, 'url', '/url'); - - this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns'); - this.handlePatterns(patchOperations, 'notifyServiceOutboundPatterns'); - - this.deletedInboundPatterns.forEach(index => { - const removeOperation: Operation = { - op: 'remove', - path: `notifyServiceInboundPatterns[${index}]` - }; - patchOperations.push(removeOperation); - }); - - this.deletedOutboundPatterns.forEach(index => { - const removeOperation: Operation = { - op: 'remove', - path: `notifyServiceOutboundPatterns[${index}]` - }; - patchOperations.push(removeOperation); - }); - - return patchOperations; - } - - onSubmit() { - this.openConfirmModal(this.confirmModal); - } - - addInboundPattern() { - const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; - notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); - } - - addOutboundPattern() { - const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; - notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup()); - } - - - toggleAutomatic(i: number) { - const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`); - if (automaticControl) { - automaticControl.setValue(!automaticControl.value); + this.formModel.patchValue({ + id: this.service.id, + name: this.service.name, + description: this.service.description, + url: this.service.url, + score: this.service.score, ldnUrl: this.service.ldnUrl, + type: this.service.type, + enabled: this.service.enabled + }); + this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns', false); + this.filterPatternObjectsAndPickLabel('notifyServiceOutboundPatterns', true); } + }, + ); + } + + /** + * Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown.. + * @param formArrayName - The name of the form array to be populated + * @param isOutbound - A boolean indicating whether the patterns are outbound (true) or inbound (false) + */ + filterPatternObjectsAndPickLabel(formArrayName: string, isOutbound: boolean) { + const PatternsArray = this.formModel.get(formArrayName) as FormArray; + PatternsArray.clear(); + let servicesToUse; + if (isOutbound) { + servicesToUse = this.service.notifyServiceOutboundPatterns; + } else { + servicesToUse = this.service.notifyServiceInboundPatterns; } - toggleEnabled() { - const newStatus = !this.formModel.get('enabled').value; + servicesToUse.forEach((patternObj: NotifyServicePattern) => { + let patternFormGroup; + if (isOutbound) { + patternFormGroup = this.initializeOutboundPatternFormGroup(); + } else { + patternFormGroup = this.initializeInboundPatternFormGroup(); + } + const newPatternObjWithLabel = Object.assign(new NotifyServicePattern(), { + ...patternObj, + patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label') + }); + patternFormGroup.patchValue(newPatternObjWithLabel); - const patchOperation: Operation = { - op: 'replace', - path: '/enabled', - value: newStatus, - }; + PatternsArray.push(patternFormGroup); + this.cdRef.detectChanges(); + }); - this.ldnServicesService.patch(this.service, [patchOperation]).pipe( - getFirstCompletedRemoteData() - ).subscribe( - () => { - this.formModel.get('enabled').setValue(newStatus); - this.cdRef.detectChanges(); - } - ); + } + + /** + * Generates an array of patch operations based on form changes + * @returns Array of patch operations + */ + generatePatchOperations(): any[] { + const patchOperations: any[] = []; + + this.createReplaceOperation(patchOperations, 'name', '/name'); + this.createReplaceOperation(patchOperations, 'description', '/description'); + this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl'); + this.createReplaceOperation(patchOperations, 'url', '/url'); + this.createReplaceOperation(patchOperations, 'score', '/score'); + + this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns'); + this.handlePatterns(patchOperations, 'notifyServiceOutboundPatterns'); + + + this.deletedInboundPatterns.forEach(index => { + const removeOperation: Operation = { + op: 'remove', + path: `notifyServiceInboundPatterns[${index}]` + }; + patchOperations.push(removeOperation); + }); + + this.deletedOutboundPatterns.forEach(index => { + const removeOperation: Operation = { + op: 'remove', + path: `notifyServiceOutboundPatterns[${index}]` + }; + patchOperations.push(removeOperation); + }); + + return patchOperations; + } + + /** + * Submits the form by opening the confirmation modal + */ + onSubmit() { + this.openConfirmModal(this.confirmModal); + } + + /** + * Adds a new inbound pattern form group to the array of inbound patterns in the form + */ + addInboundPattern() { + const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); + } + + /** + * Adds a new outbound pattern form group to the array of outbound patterns in the form + */ + addOutboundPattern() { + const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup()); + } + + /** + * Selects an outbound pattern by updating its values based on the provided pattern value and index + * @param patternValue - The selected pattern value + * @param index - The index of the outbound pattern in the array + */ + selectOutboundPattern(patternValue: string, index: number): void { + const patternArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray); + patternArray.controls[index].patchValue({pattern: patternValue}); + patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')}); + + } + + /** + * Selects an outbound item filter by updating its value based on the provided filter value and index + * @param filterValue - The selected filter value + * @param index - The index of the inbound pattern in the array + */ + selectOutboundItemFilter(filterValue: string, index: number) { + const filterArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray); + filterArray.controls[index].patchValue({constraint: filterValue}); + } + + /** + * Selects an inbound pattern by updating its values based on the provided pattern value and index + * @param patternValue - The selected pattern value + * @param index - The index of the inbound pattern in the array + */ + selectInboundPattern(patternValue: string, index: number): void { + const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray); + patternArray.controls[index].patchValue({pattern: patternValue}); + patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')}); + } + + /** + * Selects an inbound item filter by updating its value based on the provided filter value and index + * @param filterValue - The selected filter value + * @param index - The index of the inbound pattern in the array + */ + selectInboundItemFilter(filterValue: string, index: number): void { + const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray); + filterArray.controls[index].patchValue({constraint: filterValue}); + } + + /** + * Toggles the automatic property of an inbound pattern at the specified index + * @param i - The index of the inbound pattern in the array + */ + toggleAutomatic(i: number) { + const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`); + if (automaticControl) { + automaticControl.setValue(!automaticControl.value); } + } + /** + * Toggles the enabled status of the LDN service by sending a patch request + */ + toggleEnabled() { + const newStatus = !this.formModel.get('enabled').value; - closeModal() { - this.modalRef.close(); + const patchOperation: Operation = { + op: 'replace', + path: '/enabled', + value: newStatus, + }; + + this.ldnServicesService.patch(this.service, [patchOperation]).pipe( + getFirstCompletedRemoteData() + ).subscribe( + () => { + + this.formModel.get('enabled').setValue(newStatus); this.cdRef.detectChanges(); - } + } + ); + } - openConfirmModal(content) { - this.modalRef = this.modalService.open(content); - } + /** + * Closes the modal + */ + closeModal() { + this.modalRef.close(); + this.cdRef.detectChanges(); + } - openResetFormModal(content) { - this.modalRef = this.modalService.open(content); - } + /** + * Opens a confirmation modal with the specified content + * @param content - The content to be displayed in the modal + */ + openConfirmModal(content) { + this.modalRef = this.modalService.open(content); + } - patchService() { - this.deleteMarkedInboundPatterns(); - this.deleteMarkedOutboundPatterns(); - const patchOperations = this.generatePatchOperations(); + /** + * Opens a reset form modal with the specified content + * @param content - The content to be displayed in the modal + */ + openResetFormModal(content) { + this.modalRef = this.modalService.open(content); + } + + /** + * Patches the LDN service by retrieving and sending patch operations geenrated in generatePatchOperations() + */ + patchService() { + this.deleteMarkedInboundPatterns(); + this.deleteMarkedOutboundPatterns(); + + const patchOperations = this.generatePatchOperations(); - this.ldnServicesService.patch(this.service, patchOperations).pipe( - getFirstCompletedRemoteData() - ).subscribe( - () => { - - this.closeModal(); - this.sendBack(); - this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'), - this.translateService.get('admin.registries.services-formats.modify.success.content')); - } - ); - - } - - resetFormAndLeave() { - this.sendBack(); - this.closeModal(); - } - - markForInboundPatternDeletion(index: number) { - if (!this.markedForDeletionInboundPattern.includes(index)) { - this.markedForDeletionInboundPattern.push(index); + this.ldnServicesService.patch(this.service, patchOperations).pipe( + getFirstCompletedRemoteData() + ).subscribe( + (rd: RemoteData) => { + if (rd.hasSucceeded) { + this.closeModal(); + this.sendBack(); + this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'), + this.translateService.get('admin.registries.services-formats.modify.success.content')); + } else { + this.notificationService.error(this.translateService.get('admin.registries.services-formats.modify.failure.head'), + this.translateService.get('admin.registries.services-formats.modify.failure.content')); + this.closeModal(); } - } + }); + } - unmarkForInboundPatternDeletion(index: number) { - const i = this.markedForDeletionInboundPattern.indexOf(index); - if (i !== -1) { - this.markedForDeletionInboundPattern.splice(i, 1); + /** + * Resets the form and navigates back to the LDN services page + */ + resetFormAndLeave() { + this.sendBack(); + this.closeModal(); + } + + /** + * Marks the specified inbound pattern for deletion + * @param index - The index of the inbound pattern in the array + */ + markForInboundPatternDeletion(index: number) { + if (!this.markedForDeletionInboundPattern.includes(index)) { + this.markedForDeletionInboundPattern.push(index); + } + } + + /** + * Unmarks the specified inbound pattern for deletion + * @param index - The index of the inbound pattern in the array + */ + unmarkForInboundPatternDeletion(index: number) { + const i = this.markedForDeletionInboundPattern.indexOf(index); + if (i !== -1) { + this.markedForDeletionInboundPattern.splice(i, 1); + } + } + + /** + * Marks the specified outbound pattern for deletion + * @param index - The index of the outbound pattern in the array + */ + markForOutboundPatternDeletion(index: number) { + if (!this.markedForDeletionOutboundPattern.includes(index)) { + this.markedForDeletionOutboundPattern.push(index); + } + } + + /** + * Unmarks the specified outbound pattern for deletion + * @param index - The index of the outbound pattern in the array + */ + unmarkForOutboundPatternDeletion(index: number) { + const i = this.markedForDeletionOutboundPattern.indexOf(index); + if (i !== -1) { + this.markedForDeletionOutboundPattern.splice(i, 1); + } + } + + /** + * Deletes marked inbound patterns from the form model + */ + deleteMarkedInboundPatterns() { + this.markedForDeletionInboundPattern.sort((a, b) => b - a); + const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + + for (const index of this.markedForDeletionInboundPattern) { + if (index >= 0 && index < patternsArray.length) { + const patternGroup = patternsArray.at(index) as FormGroup; + const patternValue = patternGroup.value; + if (patternValue.isNew) { + patternsArray.removeAt(index); + } else { + this.deletedInboundPatterns.push(index); } + } } - markForOutboundPatternDeletion(index: number) { - if (!this.markedForDeletionOutboundPattern.includes(index)) { - this.markedForDeletionOutboundPattern.push(index); + this.markedForDeletionInboundPattern = []; + } + + /** + * Deletes marked outbound patterns from the form model + */ + deleteMarkedOutboundPatterns() { + this.markedForDeletionOutboundPattern.sort((a, b) => b - a); + const patternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + + for (const index of this.markedForDeletionOutboundPattern) { + if (index >= 0 && index < patternsArray.length) { + const patternGroup = patternsArray.at(index) as FormGroup; + const patternValue = patternGroup.value; + if (patternValue.isNew) { + patternsArray.removeAt(index); + } else { + + this.deletedOutboundPatterns.push(index); } + } } - unmarkForOutboundPatternDeletion(index: number) { - const i = this.markedForDeletionOutboundPattern.indexOf(index); - if (i !== -1) { - this.markedForDeletionOutboundPattern.splice(i, 1); + this.markedForDeletionOutboundPattern = []; + } + + /** + * Creates a replace operation and adds it to the patch operations if the form control is dirty + * @param patchOperations - The array to store patch operations + * @param formControlName - The name of the form control + * @param path - The JSON Patch path for the operation + */ + private createReplaceOperation(patchOperations: any[], formControlName: string, path: string): void { + if (this.formModel.get(formControlName).dirty) { + patchOperations.push({ + op: 'replace', + path, + value: this.formModel.get(formControlName).value, + }); + } + } + + /** + * Handles patterns in the form array, checking if an add or replace operations is required + * @param patchOperations - The array to store patch operations + * @param formArrayName - The name of the form array + */ + private handlePatterns(patchOperations: any[], formArrayName: string): void { + const patternsArray = this.formModel.get(formArrayName) as FormArray; + + + for (let i = 0; i < patternsArray.length; i++) { + const patternGroup = patternsArray.at(i) as FormGroup; + + const patternValue = patternGroup.value; + if (patternGroup.touched) { + delete patternValue?.patternLabel; + if (patternValue.isNew) { + delete patternValue.isNew; + const addOperation = { + op: 'add', + path: `${formArrayName}/-`, + value: patternValue, + }; + patchOperations.push(addOperation); + } else { + const replaceOperation = { + op: 'replace', + path: `${formArrayName}[${i}]`, + value: patternValue, + }; + patchOperations.push(replaceOperation); } + } } + } - deleteMarkedInboundPatterns() { - this.markedForDeletionInboundPattern.sort((a, b) => b - a); - const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + /** + * Navigates back to the LDN services page + */ + private sendBack() { + this.router.navigateByUrl('admin/ldn/services'); + } - for (const index of this.markedForDeletionInboundPattern) { - if (index >= 0 && index < patternsArray.length) { - const patternGroup = patternsArray.at(index) as FormGroup; - const patternValue = patternGroup.value; - if (patternValue.isNew) { - patternsArray.removeAt(index); - } else { - this.deletedInboundPatterns.push(index); - } - } - } + /** + * Creates a form group for outbound patterns + * @returns The form group for outbound patterns + */ + private createOutboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: '', + patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key), + constraint: '', + isNew: true, + }); + } - this.markedForDeletionInboundPattern = []; - } + /** + * Creates a form group for inbound patterns + * @returns The form group for inbound patterns + */ + private createInboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: '', + patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key), + constraint: '', + automatic: false, + isNew: true + }); + } + /** + * Initializes an existing form group for outbound patterns + * @returns The initialized form group for outbound patterns + */ + private initializeOutboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: '', + patternLabel: '', + constraint: '', + }); + } - deleteMarkedOutboundPatterns() { - this.markedForDeletionOutboundPattern.sort((a, b) => b - a); - const patternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; - - for (const index of this.markedForDeletionOutboundPattern) { - if (index >= 0 && index < patternsArray.length) { - const patternGroup = patternsArray.at(index) as FormGroup; - const patternValue = patternGroup.value; - if (patternValue.isNew) { - patternsArray.removeAt(index); - } else { - - this.deletedOutboundPatterns.push(index); - } - } - } - - this.markedForDeletionOutboundPattern = []; - } - - private createReplaceOperation(patchOperations: any[], formControlName: string, path: string): void { - if (this.formModel.get(formControlName).dirty) { - patchOperations.push({ - op: 'replace', - path, - value: this.formModel.get(formControlName).value, - }); - } - } - - private handlePatterns(patchOperations: any[], formArrayName: string): void { - const patternsArray = this.formModel.get(formArrayName) as FormArray; - - for (let i = 0; i < patternsArray.length; i++) { - const patternGroup = patternsArray.at(i) as FormGroup; - const patternValue = patternGroup.value; - - if (patternGroup.dirty) { - if (patternValue.isNew) { - delete patternValue.isNew; - const addOperation = { - op: 'add', - path: `${formArrayName}/-`, - value: patternValue, - }; - patchOperations.push(addOperation); - } else { - const replaceOperation = { - op: 'replace', - path: `${formArrayName}[${i}]`, - value: patternValue, - }; - patchOperations.push(replaceOperation); - } - } - } - } - - private sendBack() { - this.router.navigateByUrl('admin/ldn/services'); - } - - private createOutboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: '', - constraint: '', - isNew: true, - }); - } - - private createInboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: '', - constraint: '', - automatic: false, - isNew: true - }); - } - - private initializeOutboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: '', - constraint: '', - }); - } - - private initializeInboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: '', - constraint: '', - automatic: '', - }); - } + /** + * Initializes an existing form group for inbound patterns + * @returns The initialized form group for inbound patterns + */ + private initializeInboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: '', + patternLabel: '', + constraint: '', + automatic: '', + }); + } } diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html index f1afa886f1..f5be7c1051 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html @@ -1,266 +1,386 @@
-
-
-

{{ 'ldn-create-service.title' | translate }}

+ +
+

{{ 'ldn-create-service.title' | translate }}

+
+ +
+ + +
+ {{ 'ldn-new-service.form.error.name' | translate }} +
+
+ + +
+ + +
+ + + +
+ + +
+ {{ 'ldn-new-service.form.error.url' | translate }} +
+
+ + + +
+ + +
+ {{ 'ldn-new-service.form.error.ldnurl' | translate }} +
+
+ +
+ + +
+ {{ 'ldn-new-service.form.error.score' | translate }} +
+
+ + +
+
+ +
+ +
+
- -
- - +
+
+ +
+
+
-
-   -
+ +
- -
- - -
- -
-   -
- - -
- - -
- -
-   -
- - -
- - -
- -
-   -
- - -
-
- -
-
- -
-
- -
-
-
-
- -
- - + -
-
- -
+
+
+
+
+ + + - -
- - {{ 'ldn-new-service.form.label.addPattern' | translate }} - - -
-   -
- - -
-
- +
-
- -
-
-
-
-
-
+
-
- - - -
-
- -
-
- - - -
- -
- -
-
-
-
- -
- +
+ +
+
+ + +
+
+
-
+
+ +
+
+
+
- {{ 'ldn-new-service.form.label.addPattern' | translate }} - -
-   -
- - +
+
+
+ + {{ 'ldn-new-service.form.label.addPattern' | translate }} - + +
+
+ +
+ +
+ +
+
+
+
+
+ + +
+ + + +
+
+
+
+ + + +
+
+
+ +
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ + + + + +
-
+
- - - + + + +
-
+
- - - + + + +
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss index f0fdcd81a2..78a73a3e76 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss @@ -1,8 +1,13 @@ +@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss'; +@import '../../../shared/form/form.component.scss'; + form { - max-width: 800px; font-size: 14px; position: relative; +} +label { + font-weight: bold; } input[type="text"], @@ -30,7 +35,6 @@ textarea { .add-pattern-link { color: #0048ff; cursor: pointer; - margin-left: 10px; } .remove-pattern-link { @@ -39,6 +43,11 @@ textarea { margin-left: 10px; } +.small-text { + font-size: 0.7em; + color: #888; +} + .status-checkbox { margin-top: 5px; } @@ -49,6 +58,12 @@ textarea { color: #000000; } +.error-text { + color: red; + font-size: 0.8em; + margin-top: 5px; +} + .toggle-switch { display: flex; align-items: center; @@ -99,24 +114,6 @@ textarea { cursor: pointer; } -.label-box { - margin-left: 11px; -} - -.label-box-2 { - margin-left: 14px; -} - -.label-box-3 { - margin-left: 5px; -} - -form button.btn.btn-primary[type="submit"] { - position: absolute; - bottom: 0; - right: -10px; -} - .submission-form-footer { border-radius: var(--bs-card-border-radius); bottom: 0; diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts index 757b617091..5db8e66c33 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts @@ -1,25 +1,88 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { LdnServiceFormComponent } from './ldn-service-form.component'; +import {LdnServiceFormComponent} from './ldn-service-form.component'; +import {FormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {RouterTestingModule} from '@angular/router/testing'; +import {NgbDropdownModule, NgbModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service'; +import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service'; +import {NotificationsService} from 'src/app/shared/notifications/notifications.service'; +import {Router} from '@angular/router'; +import {RouterStub} from 'src/app/shared/testing/router.stub'; +import {createPaginatedList} from 'src/app/shared/testing/utils.test'; +import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters'; +import {createSuccessfulRemoteDataObject$} from 'src/app/shared/remote-data.utils'; +import {of} from 'rxjs'; +import {EventEmitter} from '@angular/core'; describe('LdnServiceFormComponent', () => { - let component: LdnServiceFormComponent; - let fixture: ComponentFixture; + let component: LdnServiceFormComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [LdnServiceFormComponent] - }) - .compileComponents(); + let ldnServicesService: any; + let ldnItemfiltersService: any; + let notificationsService: any; + + const itemFiltersRdPL$ = createSuccessfulRemoteDataObject$(createPaginatedList([new Itemfilter()])); + + const translateServiceStub = { + get: () => of('translated-text'), + instant: () => 'translated-text', + onLangChange: new EventEmitter(), + onTranslationChange: new EventEmitter(), + onDefaultLangChange: new EventEmitter() +}; + + beforeEach(async () => { + ldnItemfiltersService = jasmine.createSpyObj('ldnItemfiltersService', { + findAll: jasmine.createSpy('findAll'), }); - beforeEach(() => { - fixture = TestBed.createComponent(LdnServiceFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + ldnServicesService = jasmine.createSpyObj('ldnServicesService', { + create: jasmine.createSpy('create'), }); - it('should create', () => { - expect(component).toBeTruthy(); + notificationsService = jasmine.createSpyObj('notificationsService', { + success: jasmine.createSpy('success'), + error: jasmine.createSpy('error'), }); + + await TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + NgbModalModule, + TranslateModule.forRoot(), + NgbDropdownModule + ], + providers: [ + {provide: LdnItemfiltersService, useValue: ldnItemfiltersService}, + {provide: LdnServicesService, useValue: ldnServicesService}, + {provide: NotificationsService, useValue: notificationsService}, + {provide: TranslateService, useValue: translateServiceStub}, + {provide: Router, useValue: new RouterStub()}, + { + provide: NgbModal, useValue: { + open: () => {/*comment*/ + } + } + }, + FormBuilder + ], + declarations: [LdnServiceFormComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LdnServiceFormComponent); + component = fixture.componentInstance; + ldnItemfiltersService.findAll.and.returnValue(itemFiltersRdPL$); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts index 6b908cdae2..8f238a8874 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts @@ -1,222 +1,369 @@ -import { - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnInit, - Output, - TemplateRef, - ViewChild -} from '@angular/core'; -import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; - -import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; -import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patterns'; -import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; -import { animate, state, style, transition, trigger } from '@angular/animations'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { RemoteData } from '../../../core/data/remote-data'; -import { LdnService } from '../ldn-services-model/ldn-services.model'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; -import { Observable } from 'rxjs'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { LdnItemfiltersService } from '../ldn-services-data/ldn-itemfilters-data.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild} from '@angular/core'; +import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {Router} from '@angular/router'; +import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service'; +import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns'; +import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type'; +import {animate, state, style, transition, trigger} from '@angular/animations'; +import {getFirstCompletedRemoteData} from '../../../core/shared/operators'; +import {RemoteData} from '../../../core/data/remote-data'; +import {LdnService} from '../ldn-services-model/ldn-services.model'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {TranslateService} from '@ngx-translate/core'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters'; +import {Observable} from 'rxjs'; +import {FindListOptions} from '../../../core/data/find-list-options.model'; +import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model'; +import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +/** + * Angular component representing the form for creating or editing LDN services. + * This component handles the creation, validation, and submission of LDN service data. + */ @Component({ - selector: 'ds-ldn-service-form', - templateUrl: './ldn-service-form.component.html', - styleUrls: ['./ldn-service-form.component.scss'], - animations: [ - trigger('toggleAnimation', [ - state('true', style({})), - state('false', style({})), - transition('true <=> false', animate('300ms ease-in')), - ]), - ], + selector: 'ds-ldn-service-form', + templateUrl: './ldn-service-form.component.html', + styleUrls: ['./ldn-service-form.component.scss'], + animations: [ + trigger('toggleAnimation', [ + state('true', style({})), + state('false', style({})), + transition('true <=> false', animate('300ms ease-in')), + ]), + ], }) export class LdnServiceFormComponent implements OnInit { - formModel: FormGroup; - @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef; - @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef; - public inboundPatterns: object[] = notifyPatterns; - public outboundPatterns: object[] = notifyPatterns; - itemfiltersRD$: Observable>>; - config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 20 + formModel: FormGroup; + @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef; + @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef; + public inboundPatterns: string[] = notifyPatterns; + public outboundPatterns: string[] = notifyPatterns; + itemfiltersRD$: Observable>>; + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 20 + }); + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'po', + pageSize: 20 + }); + @Input() public name: string; + @Input() public description: string; + @Input() public url: string; + @Input() public score: string; + @Input() public ldnUrl: string; + @Input() public inboundPattern: string; + @Input() public outboundPattern: string; + @Input() public constraint: string; + @Input() public automatic: boolean; + @Input() public headerKey: string; + @Output() submitForm: EventEmitter = new EventEmitter(); + @Output() cancelForm: EventEmitter = new EventEmitter(); + selectedOutboundPatterns: string[]; + selectedInboundPatterns: string[]; + selectedInboundItemfilters: string[]; + selectedOutboundItemfilters: string[]; + hasInboundPattern: boolean; + hasOutboundPattern: boolean; + isScoreValid: boolean; + private modalRef: any; + private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select'; + + constructor( + private ldnServicesService: LdnServicesService, + private ldnItemfiltersService: LdnItemfiltersService, + private formBuilder: FormBuilder, + private router: Router, + private notificationsService: NotificationsService, + private translateService: TranslateService, + private cdRef: ChangeDetectorRef, + protected modalService: NgbModal, + ) { + + this.formModel = this.formBuilder.group({ + enabled: true, + id: [''], + name: ['', Validators.required], + description: [''], + url: ['', Validators.required], + score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], + ldnUrl: ['', Validators.required], + inboundPattern: [''], + outboundPattern: [''], + constraintPattern: [''], + notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), + notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]), + type: LDN_SERVICE.value, }); - pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'po', - pageSize: 20 + } + + ngOnInit(): void { + this.setItemfilters(); + + } + + /** + * Sets up the item filters by fetching and observing the paginated list of item filters. + */ + setItemfilters() { + this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe( + getFirstCompletedRemoteData()); + } + + /** + * Handles the form submission by opening the confirmation modal. + */ + onSubmit() { + this.openConfirmModal(this.confirmModal); + } + + /** + * Opens the confirmation modal. + * + * @param {any} content - The content of the modal. + */ + openConfirmModal(content) { + this.modalRef = this.modalService.open(content); + } + + /** + * Opens the reset form modal. + * + * @param {any} content - The content of the modal. + */ + openResetFormModal(content) { + this.modalRef = this.modalService.open(content); + } + + /** + * Handles the creation of an LDN service by retrieving and validating form fields, + * and submitting the form data to the LDN services endpoint. + */ + createService() { + this.formModel.get('name').markAsTouched(); + this.formModel.get('score').markAsTouched(); + this.formModel.get('url').markAsTouched(); + this.formModel.get('ldnUrl').markAsTouched(); + this.formModel.get('notifyServiceInboundPatterns').markAsTouched(); + this.formModel.get('notifyServiceOutboundPatterns').markAsTouched(); + + const name = this.formModel.get('name').value; + const url = this.formModel.get('url').value; + const score = this.formModel.get('score').value; + const ldnUrl = this.formModel.get('ldnUrl').value; + + const hasInboundPattern = this.checkPatterns(this.formModel.get('notifyServiceInboundPatterns') as FormArray); + const hasOutboundPattern = this.checkPatterns(this.formModel.get('notifyServiceOutboundPatterns') as FormArray); + + if (!name || !url || !ldnUrl || !score || (!hasInboundPattern && !hasOutboundPattern)) { + this.closeModal(); + return; + } + + this.formModel.value.notifyServiceInboundPatterns = this.formModel.value.notifyServiceInboundPatterns.map((pattern: { + pattern: string; + patternLabel: string + }) => { + const {patternLabel, ...rest} = pattern; + return rest; }); - @Input() public name: string; - @Input() public description: string; - @Input() public url: string; - @Input() public ldnUrl: string; - @Input() public inboundPattern: string; - @Input() public outboundPattern: string; - @Input() public constraint: string; - @Input() public automatic: boolean; - @Input() public headerKey: string; - @Output() submitForm: EventEmitter = new EventEmitter(); - @Output() cancelForm: EventEmitter = new EventEmitter(); - private modalRef: any; - constructor( - private ldnServicesService: LdnServicesService, - private ldnItemfiltersService: LdnItemfiltersService, - private formBuilder: FormBuilder, - private router: Router, - private notificationsService: NotificationsService, - private translateService: TranslateService, - private cdRef: ChangeDetectorRef, - protected modalService: NgbModal, - ) { + this.formModel.value.notifyServiceOutboundPatterns = this.formModel.value.notifyServiceOutboundPatterns.map((pattern: { + pattern: string; + patternLabel: string + }) => { + const {patternLabel, ...rest} = pattern; + return rest; + }); - this.formModel = this.formBuilder.group({ - enabled: true, - id: [''], - name: ['', Validators.required], - description: [''], - url: ['', Validators.required], - ldnUrl: ['', Validators.required], - inboundPattern: [''], - outboundPattern: [''], - constraintPattern: [''], - notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), - notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]), - type: LDN_SERVICE.value, - }); - } + const values = this.formModel.value; - ngOnInit(): void { - this.setItemfilters(); + const ldnServiceData = this.ldnServicesService.create(values); - } + ldnServiceData.pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get('ldn-service-notification.created.success.title'), + this.translateService.get('ldn-service-notification.created.success.body')); - setItemfilters() { - this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe( - getFirstCompletedRemoteData()); - } - - onSubmit() { - this.openConfirmModal(this.confirmModal); - } - - openConfirmModal(content) { - this.modalRef = this.modalService.open(content); - } - - openResetFormModal(content) { - this.modalRef = this.modalService.open(content); - } - - createService() { - this.formModel.get('name').markAsTouched(); - this.formModel.get('url').markAsTouched(); - this.formModel.get('ldnUrl').markAsTouched(); - - const name = this.formModel.get('name').value; - const url = this.formModel.get('url').value; - const ldnUrl = this.formModel.get('ldnUrl').value; - - if (!name || !url || !ldnUrl) { - this.closeModal(); - return; - } - - const values = this.formModel.value; - - const inboundPatternValue = this.formModel.get('inboundPattern').value; - const outboundPatternValue = this.formModel.get('outboundPattern').value; - - if (inboundPatternValue === '') { - values.notifyServiceInboundPatterns = []; - } - if (outboundPatternValue === '') { - values.notifyServiceOutboundPatterns = []; - } - - const ldnServiceData = this.ldnServicesService.create(values); - - ldnServiceData.pipe( - getFirstCompletedRemoteData() - ).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get('ldn-service-notification.created.success.title'), - this.translateService.get('ldn-service-notification.created.success.body')); - - this.sendBack(); - this.closeModal(); - } else { - this.notificationsService.error(this.translateService.get('notification.created.failure')); - } - }); - } - - - resetFormAndLeave() { this.sendBack(); this.closeModal(); - } + } else { + this.notificationsService.error(this.translateService.get('ldn-service-notification.created.failure.title'), + this.translateService.get('ldn-service-notification.created.failure.body')); + this.closeModal(); + } + }); + } - closeModal() { - this.modalRef.close(); - this.cdRef.detectChanges(); + /** + * Checks if at least one pattern in the specified form array has a value. + * + * @param {FormArray} formArray - The form array containing patterns to check. + * @returns {boolean} - True if at least one pattern has a value, otherwise false. + */ + checkPatterns(formArray: FormArray): boolean { + for (let i = 0; i < formArray.length; i++) { + const pattern = formArray.at(i).get('pattern').value; + if (pattern) { + return true; + } } + return false; + } - addInboundPattern() { - const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; - notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); + /** + * Closes the currently open modal and returns to the services directory.. + */ + resetFormAndLeave() { + this.sendBack(); + this.closeModal(); + } + + /** + * Closes the currently open modal and triggers change detection. + */ + closeModal() { + this.modalRef.close(); + this.cdRef.detectChanges(); + } + + /** + * Adds a new inbound pattern form group to the notifyServiceInboundPatterns form array. + */ + addInboundPattern() { + const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); + } + + /** + * Removes the inbound pattern form group at the specified index from the notifyServiceInboundPatterns form array. + * + * @param {number} index - The index of the inbound pattern form group to remove. + * @memberof LdnServiceFormComponent + */ + removeInboundPattern(index: number) { + const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; + notifyServiceInboundPatternsArray.removeAt(index); + } + + /** + * Adds a new outbound pattern form group to the notifyServiceOutboundPatterns form array. + */ + addOutboundPattern() { + const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup()); + } + + /** + * Removes the outbound pattern form group at the specified index from the notifyServiceOutboundPatterns form array. + * + * @param {number} index - The index of the outbound pattern form group to remove. + */ + removeOutboundPattern(index: number) { + const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; + notifyServiceOutboundPatternsArray.removeAt(index); + } + + /** + * Toggles the value of the 'automatic' control at the specified index in the notifyServiceInboundPatterns form array. + * + * @param {number} i - The index of the 'automatic' control to toggle. + * @memberof LdnServiceFormComponent + */ + toggleAutomatic(i: number) { + const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`); + if (automaticControl) { + automaticControl.setValue(!automaticControl.value); } + } - removeInboundPattern(index: number) { - const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; - notifyServiceInboundPatternsArray.removeAt(index); - } + /** + * Selects an outbound pattern for a specific index in the notifyServiceOutboundPatterns form array. + * + * @param {string} patternValue - The selected pattern value. + * @param {number} index - The index of the outbound pattern in the form array. + */ + selectOutboundPattern(patternValue: string, index: number): void { + const patternArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray); + patternArray.controls[index].patchValue({pattern: patternValue}); + patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')}); - addOutboundPattern() { - const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; - notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup()); - } + } - removeOutboundPattern(index: number) { - const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; - notifyServiceOutboundPatternsArray.removeAt(index); - } + /** + * Selects an inbound pattern for a specific index in the form array. + * + * @param {string} patternValue - The selected pattern value. + * @param {number} index - The index of the inbound pattern in the form array. + */ + selectInboundPattern(patternValue: string, index: number): void { + const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray); + patternArray.controls[index].patchValue({pattern: patternValue}); + patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')}); - toggleAutomatic(i: number) { - const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`); - if (automaticControl) { - automaticControl.setValue(!automaticControl.value); - } - } + } - private sendBack() { - this.router.navigateByUrl('admin/ldn/services'); - } + /** + * Selects an inbound item filter for a specific index in the form array. + * + * @param {string} filterValue - The selected item filter value. + * @param {number} index - The index of the inbound item filter in the form array. + */ + selectInboundItemFilter(filterValue: string, index: number): void { + const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray); + filterArray.controls[index].patchValue({constraint: filterValue}); + } + /** + * Selects an outbound item filter for a specific index in the form array. + * + * @param {string} filterValue - The selected item filter value. + * @param {number} index - The index of the outbound item filter in the form array. + */ + selectOutboundItemFilter(filterValue: string, index: number) { + const filterArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray); + filterArray.controls[index].patchValue({constraint: filterValue}); + } - private createOutboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: [''], - constraint: [''], - }); - } + /** + * Sends the user back to the LDN services list. + */ + private sendBack() { + this.router.navigateByUrl('admin/ldn/services'); + } - private createInboundPatternFormGroup(): FormGroup { - return this.formBuilder.group({ - pattern: [''], - constraint: [''], - automatic: false - }); - } + /** + * Creates a form group for an outbound pattern in the notifyServiceOutboundPatterns form array. + * + * @private + * @returns {FormGroup} - The created form group. + */ + private createOutboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: [''], + constraint: [''], + patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key), + }); + } + + /** + * Creates a form group for an inbound pattern in the notifyServiceInboundPatterns form array. + * + * @private + * @returns {FormGroup} - The created form group. + */ + private createInboundPatternFormGroup(): FormGroup { + return this.formBuilder.group({ + pattern: [''], + constraint: [''], + automatic: false, + patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key), + }); + } } diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts index d49d393195..ced1371623 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.spec.ts @@ -1,25 +1,25 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { LdnServiceNewComponent } from './ldn-service-new.component'; +import {LdnServiceNewComponent} from './ldn-service-new.component'; describe('LdnServiceNewComponent', () => { - let component: LdnServiceNewComponent; - let fixture: ComponentFixture; + let component: LdnServiceNewComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [LdnServiceNewComponent] - }) - .compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LdnServiceNewComponent] + }) + .compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(LdnServiceNewComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(LdnServiceNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts index e92c06dc26..773f042d11 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-new/ldn-service-new.component.ts @@ -1,27 +1,9 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { LdnService } from "../ldn-services-model/ldn-services.model"; -import { ActivatedRoute } from "@angular/router"; -import { ProcessDataService } from "../../../core/data/processes/process-data.service"; -import { LinkService } from "../../../core/cache/builders/link.service"; +import {Component} from '@angular/core'; @Component({ - selector: 'ds-ldn-service-new', - templateUrl: './ldn-service-new.component.html', - styleUrls: ['./ldn-service-new.component.scss'] + selector: 'ds-ldn-service-new', + templateUrl: './ldn-service-new.component.html', + styleUrls: ['./ldn-service-new.component.scss'] }) -export class LdnServiceNewComponent implements OnInit { - /** - * Emits preselected process if there is one - */ - ldnService$?: Observable; - - constructor(private route: ActivatedRoute, private processService: ProcessDataService, private linkService: LinkService) { - } - - /** - * If there's an id parameter, use this the process with this identifier as presets for the form - */ - ngOnInit() { - } +export class LdnServiceNewComponent { } diff --git a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts index b1a954b9d6..02090b1602 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts @@ -1,69 +1,126 @@ -import { LdnService } from '../ldn-services-model/ldn-services.model'; -import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { Observable, of } from 'rxjs'; -// Create a mock data object for a single LDN notify service +import {LdnService} from '../ldn-services-model/ldn-services.model'; +import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type'; +import {RemoteData} from '../../../core/data/remote-data'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {Observable, of} from 'rxjs'; +import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils'; + export const mockLdnService: LdnService = { - id: 1, - name: 'Service Name', - description: 'Service Description', - url: 'Service URL', - ldnUrl: 'Service LDN URL', - notifyServiceInboundPatterns: [ - { - pattern: 'patternA', - constraint: 'itemFilterA', - automatic: false, - }, - { - pattern: 'patternB', - constraint: 'itemFilterB', - automatic: true, - }, - ], - notifyServiceOutboundPatterns: [ - { - pattern: 'patternC', - constraint: 'itemFilterC', - }, - ], - type: LDN_SERVICE, - _links: { - self: { - href: 'http://localhost/api/ldn/ldnservices/1', - }, + uuid: '1', + enabled: false, + score: 0, + id: 1, + name: 'Service Name', + description: 'Service Description', + url: 'Service URL', + ldnUrl: 'Service LDN URL', + notifyServiceInboundPatterns: [ + { + pattern: 'patternA', + constraint: 'itemFilterA', + automatic: 'false', }, + { + pattern: 'patternB', + constraint: 'itemFilterB', + automatic: 'true', + }, + ], + notifyServiceOutboundPatterns: [ + { + pattern: 'patternC', + constraint: 'itemFilterC', + automatic: 'true', + }, + ], + type: LDN_SERVICE, + _links: { + self: { + href: 'http://localhost/api/ldn/ldnservices/1' + }, + }, + get self(): string { + return ''; + }, }; +export const mockLdnServiceRD$ = createSuccessfulRemoteDataObject$(mockLdnService); -const mockLdnServices = { - payload: { - elementsPerPage: 20, - totalPages: 1, - totalElements: 1, - currentPage: 1, - first: undefined, - prev: undefined, - next: undefined, - last: undefined, - page: [mockLdnService], - type: LDN_SERVICE, - self: undefined, - getPageLength: function () { - return this.page.length; - }, - _links: { - self: { - href: 'http://localhost/api/ldn/ldnservices/1', - }, - page: [], - }, + +export const mockLdnServices: LdnService[] = [{ + uuid: '1', + enabled: false, + score: 0, + id: 1, + name: 'Service Name', + description: 'Service Description', + url: 'Service URL', + ldnUrl: 'Service LDN URL', + notifyServiceInboundPatterns: [ + { + pattern: 'patternA', + constraint: 'itemFilterA', + automatic: 'false', }, - hasSucceeded: true, - msToLive: 0, -}; - - -// Create a mock ldnServicesRD$ observable + { + pattern: 'patternB', + constraint: 'itemFilterB', + automatic: 'true', + }, + ], + notifyServiceOutboundPatterns: [ + { + pattern: 'patternC', + constraint: 'itemFilterC', + automatic: 'true', + }, + ], + type: LDN_SERVICE, + _links: { + self: { + href: 'http://localhost/api/ldn/ldnservices/1' + }, + }, + get self(): string { + return ''; + }, +}, { + uuid: '2', + enabled: false, + score: 0, + id: 2, + name: 'Service Name', + description: 'Service Description', + url: 'Service URL', + ldnUrl: 'Service LDN URL', + notifyServiceInboundPatterns: [ + { + pattern: 'patternA', + constraint: 'itemFilterA', + automatic: 'false', + }, + { + pattern: 'patternB', + constraint: 'itemFilterB', + automatic: 'true', + }, + ], + notifyServiceOutboundPatterns: [ + { + pattern: 'patternC', + constraint: 'itemFilterC', + automatic: 'true', + }, + ], + type: LDN_SERVICE, + _links: { + self: { + href: 'http://localhost/api/ldn/ldnservices/1' + }, + }, + get self(): string { + return ''; + }, +} +]; export const mockLdnServicesRD$: Observable>> = of((mockLdnServices as unknown) as RemoteData>); diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts index babb00c959..15a7bcccda 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts @@ -1,20 +1,20 @@ -import { Injectable } from '@angular/core'; -import { dataService } from '../../../core/data/base/data-service.decorator'; -import { LDN_SERVICE_CONSTRAINT_FILTERS } from '../ldn-services-model/ldn-service.resource-type'; -import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; +import {Injectable} from '@angular/core'; +import {dataService} from '../../../core/data/base/data-service.decorator'; +import {LDN_SERVICE_CONSTRAINT_FILTERS} from '../ldn-services-model/ldn-service.resource-type'; +import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service'; +import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data'; -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 { FindListOptions } from '../../../core/data/find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; +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 {FindListOptions} from '../../../core/data/find-list-options.model'; +import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model'; +import {Observable} from 'rxjs'; +import {RemoteData} from '../../../core/data/remote-data'; +import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; /** @@ -23,25 +23,39 @@ import { PaginatedList } from '../../../core/data/paginated-list.model'; @Injectable() @dataService(LDN_SERVICE_CONSTRAINT_FILTERS) export class LdnItemfiltersService extends IdentifiableDataService implements FindAllData { - private findAllData: FindAllDataImpl; + private findAllData: FindAllDataImpl; - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - ) { - super('itemfilters', requestService, rdbService, objectCache, halService); + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('itemfilters', requestService, rdbService, objectCache, halService); - this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); - } + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } - getEndpoint() { - return this.halService.getEndpoint(this.linkPath); - } + /** + * Gets the endpoint URL for the itemfilters. + * + * @returns {string} - The endpoint URL. + */ + getEndpoint() { + return this.halService.getEndpoint(this.linkPath); + } - findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } + /** + * Finds all itemfilters based on the provided options and link configurations. + * + * @param {FindListOptions} options - The options for finding a list of itemfilters. + * @param {boolean} useCachedVersionIfAvailable - Whether to use the cached version if available. + * @param {boolean} reRequestOnStale - Whether to re-request the data if it's stale. + * @param {...FollowLinkConfig[]} linksToFollow - Configurations for following specific links. + * @returns {Observable>>} - An observable of remote data containing a paginated list of itemfilters. + */ + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts index 35f9bee04f..e7c2f47159 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts @@ -1,126 +1,208 @@ -import { Injectable } from '@angular/core'; -import { dataService } from '../../../core/data/base/data-service.decorator'; -import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; -import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; -import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data'; -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 { FindListOptions } from '../../../core/data/find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { NoContent } from '../../../core/shared/NoContent.model'; -import { map, take } from 'rxjs/operators'; -import { URLCombiner } from '../../../core/url-combiner/url-combiner'; -import { MultipartPostRequest } from '../../../core/data/request.models'; -import { RestRequest } from '../../../core/data/rest-request.model'; +import {Injectable} from '@angular/core'; +import {dataService} from '../../../core/data/base/data-service.decorator'; +import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type'; +import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service'; +import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data'; +import {DeleteData, DeleteDataImpl} from '../../../core/data/base/delete-data'; +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 {FindListOptions} from '../../../core/data/find-list-options.model'; +import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model'; +import {Observable} from 'rxjs'; +import {RemoteData} from '../../../core/data/remote-data'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {NoContent} from '../../../core/shared/NoContent.model'; +import {map, take} from 'rxjs/operators'; +import {URLCombiner} from '../../../core/url-combiner/url-combiner'; +import {MultipartPostRequest} from '../../../core/data/request.models'; +import {RestRequest} from '../../../core/data/rest-request.model'; -import { LdnService } from '../ldn-services-model/ldn-services.model'; +import {LdnService} from '../ldn-services-model/ldn-services.model'; -import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data'; -import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; -import { Operation } from 'fast-json-patch'; -import { RestRequestMethod } from 'src/app/core/data/rest-request-method'; -import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data'; -import { ldnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model'; -import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; -import { hasValue } from 'src/app/shared/empty.util'; +import {PatchData, PatchDataImpl} from '../../../core/data/base/patch-data'; +import {ChangeAnalyzer} from '../../../core/data/change-analyzer'; +import {Operation} from 'fast-json-patch'; +import {RestRequestMethod} from '../../../core/data/rest-request-method'; +import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data'; +import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model'; +import {getFirstCompletedRemoteData} from '../../../core/shared/operators'; +import {hasValue} from '../../../shared/empty.util'; +import {SearchDataImpl} from '../../../core/data/base/search-data'; +import {RequestParam} from '../../../core/cache/models/request-param.model'; /** - * A service responsible for fetching/sending data from/to the REST API on the ldnservices endpoint + * Injectable service responsible for fetching/sending data from/to the REST API on the ldnservices endpoint. + * + * @export + * @class LdnServicesService + * @extends {IdentifiableDataService} + * @implements {FindAllData} + * @implements {DeleteData} + * @implements {PatchData} + * @implements {CreateData} */ @Injectable() @dataService(LDN_SERVICE) export class LdnServicesService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData { - createData: CreateDataImpl; - private findAllData: FindAllDataImpl; - private deleteData: DeleteDataImpl; - private patchData: PatchDataImpl; - private comparator: ChangeAnalyzer; + createData: CreateDataImpl; + private findAllData: FindAllDataImpl; + private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; + private comparator: ChangeAnalyzer; + private searchData: SearchDataImpl; - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - ) { - super('ldnservices', requestService, rdbService, objectCache, halService); + private findByPatternEndpoint = 'byInboundPattern'; - this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); - this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); - this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); - this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); - } + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('ldnservices', requestService, rdbService, objectCache, halService); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); + } - create(object: LdnService): Observable> { - return this.createData.create(object); - } + /** + * Creates an LDN service by sending a POST request to the REST API. + * + * @param {LdnService} object - The LDN service object to be created. + * @returns {Observable>} - Observable containing the result of the creation operation. + */ + create(object: LdnService): Observable> { + return this.createData.create(object); + } - patch(object: LdnService, operations: Operation[]): Observable> { - return this.patchData.patch(object, operations); - } + /** + * Updates an LDN service by applying a set of operations through a PATCH request to the REST API. + * + * @param {LdnService} object - The LDN service object to be updated. + * @param {Operation[]} operations - The patch operations to be applied. + * @returns {Observable>} - Observable containing the result of the update operation. + */ + patch(object: LdnService, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } - update(object: LdnService): Observable> { - return this.patchData.update(object); - } + /** + * Updates an LDN service by sending a PUT request to the REST API. + * + * @param {LdnService} object - The LDN service object to be updated. + * @returns {Observable>} - Observable containing the result of the update operation. + */ + update(object: LdnService): Observable> { + return this.patchData.update(object); + } - commitUpdates(method?: RestRequestMethod): void { - return this.patchData.commitUpdates(method); - } + /** + * Commits pending updates by sending a PATCH request to the REST API. + * + * @param {RestRequestMethod} [method] - The HTTP method to be used for the request. + */ + commitUpdates(method?: RestRequestMethod): void { + return this.patchData.commitUpdates(method); + } - createPatchFromCache(object: LdnService): Observable { - return this.patchData.createPatchFromCache(object); - } + /** + * Creates a patch representing the changes made to the LDN service in the cache. + * + * @param {LdnService} object - The LDN service object for which to create the patch. + * @returns {Observable} - Observable containing the patch operations. + */ + createPatchFromCache(object: LdnService): Observable { + return this.patchData.createPatchFromCache(object); + } - findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } + /** + * Retrieves all LDN services from the REST API based on the provided options. + * + * @param {FindListOptions} [options] - The options to be applied to the request. + * @param {boolean} [useCachedVersionIfAvailable] - Flag indicating whether to use cached data if available. + * @param {boolean} [reRequestOnStale] - Flag indicating whether to re-request data if it's stale. + * @param {...FollowLinkConfig[]} linksToFollow - Optional links to follow during the request. + * @returns {Observable>>} - Observable containing the result of the request. + */ + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } - public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { - return this.deleteData.delete(objectId, copyVirtualMetadata); - } + /** + * Retrieves LDN services based on the inbound pattern from the REST API. + * + * @param {string} pattern - The inbound pattern to be used in the search. + * @param {FindListOptions} [options] - The options to be applied to the request. + * @param {boolean} [useCachedVersionIfAvailable] - Flag indicating whether to use cached data if available. + * @param {boolean} [reRequestOnStale] - Flag indicating whether to re-request data if it's stale. + * @param {...FollowLinkConfig[]} linksToFollow - Optional links to follow during the request. + * @returns {Observable>>} - Observable containing the result of the request. + */ + findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + const params = [new RequestParam('pattern', pattern)]; + const findListOptions = Object.assign(new FindListOptions(), options, {searchParams: params}); + return this.searchData.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } - public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { - return this.deleteData.deleteByHref(href, copyVirtualMetadata); - } + /** + * Deletes an LDN service by sending a DELETE request to the REST API. + * + * @param {string} objectId - The ID of the LDN service to be deleted. + * @param {string[]} [copyVirtualMetadata] - Optional virtual metadata to be copied during the deletion. + * @returns {Observable>} - Observable containing the result of the deletion operation. + */ + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } - public invoke(serviceName: string, serviceId: string, parameters: ldnServiceConstrain[], files: File[]): Observable> { - const requestId = this.requestService.generateRequestId(); - this.getBrowseEndpoint().pipe( - take(1), - map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'processes', serviceId).toString()), - map((endpoint: string) => { - const body = this.getInvocationFormData(parameters, files); - return new MultipartPostRequest(requestId, endpoint, body); - }) - ).subscribe((request: RestRequest) => this.requestService.send(request)); + /** + * Deletes an LDN service by its HATEOAS link. + * + * @param {string} href - The HATEOAS link of the LDN service to be deleted. + * @param {string[]} [copyVirtualMetadata] - Optional virtual metadata to be copied during the deletion. + * @returns {Observable>} - Observable containing the result of the deletion operation. + */ + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } - return this.rdbService.buildFromRequestUUID(requestId); - } + public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable> { + const requestId = this.requestService.generateRequestId(); + this.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'processes', serviceId).toString()), + map((endpoint: string) => { + const body = this.getInvocationFormData(parameters, files); + return new MultipartPostRequest(requestId, endpoint, body); + }) + ).subscribe((request: RestRequest) => this.requestService.send(request)); - public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable { - return this.findById(scriptName).pipe( - getFirstCompletedRemoteData(), - map((rd: RemoteData) => { - return hasValue(rd.payload); - }), - ); - } + return this.rdbService.buildFromRequestUUID(requestId); + } - private getInvocationFormData(constrain: ldnServiceConstrain[], files: File[]): FormData { - const form: FormData = new FormData(); - form.set('properties', JSON.stringify(constrain)); - files.forEach((file: File) => { - form.append('file', file); - }); - return form; - } + public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable { + return this.findById(scriptName).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + return hasValue(rd.payload); + }), + ); + } + + private getInvocationFormData(constrain: LdnServiceConstrain[], files: File[]): FormData { + const form: FormData = new FormData(); + form.set('properties', JSON.stringify(constrain)); + files.forEach((file: File) => { + form.append('file', file); + }); + return form; + } } diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html index 45c2106468..e19f986e1a 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html @@ -1,83 +1,84 @@
-
-

{{ 'ldn-registered-services.title' | translate }}

-
-
- -
- -
- - - - - - - - - - - - - - + + + +
{{ 'service.overview.table.name' | translate }}{{ 'service.overview.table.description' | translate }}{{ 'service.overview.table.status' | translate }}{{ 'service.overview.table.actions' | translate }}
{{ ldnService.name }}{{ ldnService.description }} +
+

{{ 'ldn-registered-services.title' | translate }}

+
+
+ +
+ +
+ + + + + + + + + + + + + + - - - -
{{ 'service.overview.table.name' | translate }}{{ 'service.overview.table.description' | translate }}{{ 'service.overview.table.status' | translate }}{{ 'service.overview.table.actions' | translate }}
{{ ldnService.name }}{{ ldnService.description }} {{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }} - -
- - -
-
-
-
+
+
+ + +
+
+
+
-
+
- - - + + + +
diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts index 0999c82c19..664edcb27d 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts @@ -1,25 +1,144 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {ChangeDetectorRef, EventEmitter} from '@angular/core'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service'; +import {PaginationService} from '../../../core/pagination/pagination.service'; +import {PaginationServiceStub} from '../../../shared/testing/pagination-service.stub'; +import {of} from 'rxjs'; +import {LdnService} from '../ldn-services-model/ldn-services.model'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {RemoteData} from '../../../core/data/remote-data'; +import {LdnServicesOverviewComponent} from './ldn-services-directory.component'; +import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils'; +import {createPaginatedList} from '../../../shared/testing/utils.test'; -import { ServicesDirectoryComponent } from './services-directory.component'; +describe('LdnServicesOverviewComponent', () => { + let component: LdnServicesOverviewComponent; + let fixture: ComponentFixture; + let ldnServicesService; + let paginationService; + let modalService: NgbModal; + let notificationsService: NotificationsService; + let translateService: TranslateService; -describe('ServicesDirectoryComponent', () => { - let component: ServicesDirectoryComponent; - let fixture: ComponentFixture; + const translateServiceStub = { + get: () => of('translated-text'), + onLangChange: new EventEmitter(), + onTranslationChange: new EventEmitter(), + onDefaultLangChange: new EventEmitter() + }; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ServicesDirectoryComponent] - }) - .compileComponents(); + beforeEach(async () => { + paginationService = new PaginationServiceStub(); + ldnServicesService = jasmine.createSpyObj('LdnServicesService', ['findAll', 'delete', 'patch']); + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LdnServicesOverviewComponent], + providers: [ + { + provide: LdnServicesService, + useValue: ldnServicesService + }, + {provide: PaginationService, useValue: paginationService}, + { + provide: NgbModal, useValue: { + open: () => { /*comment*/ + } + } + }, + {provide: ChangeDetectorRef, useValue: {}}, + {provide: NotificationsService, useValue: NotificationsServiceStub}, + {provide: TranslateService, useValue: translateServiceStub}, + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LdnServicesOverviewComponent); + component = fixture.componentInstance; + ldnServicesService = TestBed.inject(LdnServicesService); + paginationService = TestBed.inject(PaginationService); + modalService = TestBed.inject(NgbModal); + notificationsService = TestBed.inject(NotificationsService); + translateService = TestBed.inject(TranslateService); + component.modalRef = jasmine.createSpyObj({close: null}); + component.isProcessingSub = jasmine.createSpyObj({unsubscribe: null}); + component.ldnServicesRD$ = of({} as RemoteData>); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should call setLdnServices', fakeAsync(() => { + spyOn(component, 'setLdnServices').and.callThrough(); + component.ngOnInit(); + tick(); + expect(component.setLdnServices).toHaveBeenCalled(); + })); + + it('should set ldnServicesRD$ with mock data', fakeAsync(() => { + spyOn(component, 'setLdnServices').and.callThrough(); + const testData: LdnService[] = Object.assign([new LdnService()], [ + {id: 1, name: 'Service 1', description: 'Description 1', enabled: true}, + {id: 2, name: 'Service 2', description: 'Description 2', enabled: false}, + {id: 3, name: 'Service 3', description: 'Description 3', enabled: true}]); + + const mockLdnServicesRD = createPaginatedList(testData); + component.ldnServicesRD$ = createSuccessfulRemoteDataObject$(mockLdnServicesRD); + fixture.detectChanges(); + + const tableRows = fixture.debugElement.nativeElement.querySelectorAll('tbody tr'); + expect(tableRows.length).toBe(testData.length); + const firstRowContent = tableRows[0].textContent; + expect(firstRowContent).toContain('Service 1'); + expect(firstRowContent).toContain('Description 1'); + })); + }); + + describe('ngOnDestroy', () => { + it('should call paginationService.clearPagination and unsubscribe', () => { + // spyOn(paginationService, 'clearPagination'); + // spyOn(component.isProcessingSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(paginationService.clearPagination).toHaveBeenCalledWith(component.pageConfig.id); + expect(component.isProcessingSub.unsubscribe).toHaveBeenCalled(); }); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ServicesDirectoryComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + describe('openDeleteModal', () => { + it('should open delete modal', () => { + spyOn(modalService, 'open'); + component.openDeleteModal(component.deleteModal); + expect(modalService.open).toHaveBeenCalledWith(component.deleteModal); }); + }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('closeModal', () => { + it('should close modal and detect changes', () => { + // spyOn(component.modalRef, 'close'); + spyOn(component.cdRef, 'detectChanges'); + component.closeModal(); + expect(component.modalRef.close).toHaveBeenCalled(); + expect(component.cdRef.detectChanges).toHaveBeenCalled(); }); + }); + + describe('deleteSelected', () => { + it('should delete selected service and update data', fakeAsync(() => { + const serviceId = '123'; + const mockRemoteData = { /* just an empty object to retrieve as as RemoteData> */}; + spyOn(component, 'setLdnServices').and.callThrough(); + const deleteSpy = ldnServicesService.delete.and.returnValue(of(mockRemoteData as RemoteData>)); + component.selectedServiceId = serviceId; + component.deleteSelected(serviceId, ldnServicesService); + tick(); + expect(deleteSpy).toHaveBeenCalledWith(serviceId); + })); + }); }); diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts index 3db9231ac2..826b4b4154 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts @@ -7,140 +7,170 @@ import { TemplateRef, ViewChild } from '@angular/core'; -import { Observable, Subscription } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; -import { LdnService } from '../ldn-services-model/ldn-services.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { map, switchMap } from 'rxjs/operators'; -import { LdnServicesService } from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; -import { PaginationService } from 'src/app/core/pagination/pagination.service'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { hasValue } from '../../../shared/empty.util'; -import { Operation } from 'fast-json-patch'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; +import {Observable, Subscription} from 'rxjs'; +import {RemoteData} from '../../../core/data/remote-data'; +import {PaginatedList} from '../../../core/data/paginated-list.model'; +import {FindListOptions} from '../../../core/data/find-list-options.model'; +import {LdnService} from '../ldn-services-model/ldn-services.model'; +import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model'; +import {map, switchMap} from 'rxjs/operators'; +import {LdnServicesService} from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import {PaginationService} from 'src/app/core/pagination/pagination.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {hasValue} from '../../../shared/empty.util'; +import {Operation} from 'fast-json-patch'; +import {getFirstCompletedRemoteData} from '../../../core/shared/operators'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {TranslateService} from '@ngx-translate/core'; +/** + * The `LdnServicesOverviewComponent` is a component that provides an overview of LDN (Linked Data Notifications) services. + * It displays a paginated list of LDN services, allows users to edit and delete services, + * toggle the status of each service directly form the page and allows for creation of new services redirecting the user on the creation/edit form + */ @Component({ - selector: 'ds-ldn-services-directory', - templateUrl: './ldn-services-directory.component.html', - styleUrls: ['./ldn-services-directory.component.scss'], - changeDetection: ChangeDetectionStrategy.Default + selector: 'ds-ldn-services-directory', + templateUrl: './ldn-services-directory.component.html', + styleUrls: ['./ldn-services-directory.component.scss'], + changeDetection: ChangeDetectionStrategy.Default }) export class LdnServicesOverviewComponent implements OnInit, OnDestroy { - selectedServiceId: string | number | null = null; - servicesData: any[] = []; - @ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef; - ldnServicesRD$: Observable>>; - config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 20 - }); - pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'po', - pageSize: 20 - }); - isProcessingSub: Subscription; - private modalRef: any; + selectedServiceId: string | number | null = null; + servicesData: any[] = []; + @ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef; + ldnServicesRD$: Observable>>; + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 20 + }); + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'po', + pageSize: 20 + }); + isProcessingSub: Subscription; + modalRef: any; - constructor( - protected ldnServicesService: LdnServicesService, - protected paginationService: PaginationService, - protected modalService: NgbModal, - private cdRef: ChangeDetectorRef, - private notificationService: NotificationsService, - private translateService: TranslateService, - ) { + constructor( + protected ldnServicesService: LdnServicesService, + protected paginationService: PaginationService, + protected modalService: NgbModal, + public cdRef: ChangeDetectorRef, + private notificationService: NotificationsService, + private translateService: TranslateService, + ) { + } + + ngOnInit(): void { + this.setLdnServices(); + } + + /** + * Sets up the LDN services by fetching and observing the paginated list of services. + */ + setLdnServices() { + this.ldnServicesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( + switchMap((config) => this.ldnServicesService.findAll(config, false, false).pipe( + getFirstCompletedRemoteData() + )) + ); + } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.pageConfig.id); + if (hasValue(this.isProcessingSub)) { + this.isProcessingSub.unsubscribe(); } + } - ngOnInit(): void { - this.setLdnServices(); - } + /** + * Opens the delete confirmation modal. + * + * @param {any} content - The content of the modal. + */ + openDeleteModal(content) { + this.modalRef = this.modalService.open(content); + } - setLdnServices() { - this.ldnServicesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( - switchMap((config) => this.ldnServicesService.findAll(config, false, false).pipe( - getFirstCompletedRemoteData() - )) - ); - } + /** + * Closes the currently open modal and triggers change detection. + */ + closeModal() { + this.modalRef.close(); + this.cdRef.detectChanges(); + } - ngOnDestroy(): void { - this.paginationService.clearPagination(this.pageConfig.id); - if (hasValue(this.isProcessingSub)) { - this.isProcessingSub.unsubscribe(); + /** + * Sets the selected LDN service ID for deletion and opens the delete confirmation modal. + * + * @param {number} serviceId - The ID of the service to be deleted. + */ + selectServiceToDelete(serviceId: number) { + this.selectedServiceId = serviceId; + this.openDeleteModal(this.deleteModal); + } + + /** + * Deletes the selected LDN service. + * + * @param {string} serviceId - The ID of the service to be deleted. + * @param {LdnServicesService} ldnServicesService - The service for managing LDN services. + */ + deleteSelected(serviceId: string, ldnServicesService: LdnServicesService): void { + if (this.selectedServiceId !== null) { + ldnServicesService.delete(serviceId).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.servicesData = this.servicesData.filter(service => service.id !== serviceId); + this.ldnServicesRD$ = this.ldnServicesRD$.pipe( + map((remoteData: RemoteData>) => { + if (remoteData.hasSucceeded) { + remoteData.payload.page = remoteData.payload.page.filter(service => service.id.toString() !== serviceId); + } + return remoteData; + }) + ); + this.cdRef.detectChanges(); + this.closeModal(); + this.notificationService.success(this.translateService.get('ldn-service-delete.notification.success.title'), + this.translateService.get('ldn-service-delete.notification.success.content')); + } else { + this.notificationService.error(this.translateService.get('ldn-service-delete.notification.error.title'), + this.translateService.get('ldn-service-delete.notification.error.content')); + this.cdRef.detectChanges(); } + }); } + } - openDeleteModal(content) { - this.modalRef = this.modalService.open(content); - } + /** + * Toggles the status (enabled/disabled) of an LDN service. + * + * @param {any} ldnService - The LDN service object. + * @param {LdnServicesService} ldnServicesService - The service for managing LDN services. + */ + toggleStatus(ldnService: any, ldnServicesService: LdnServicesService): void { + const newStatus = !ldnService.enabled; + const originalStatus = ldnService.enabled; - closeModal() { - this.modalRef.close(); - this.cdRef.detectChanges(); - } + const patchOperation: Operation = { + op: 'replace', + path: '/enabled', + value: newStatus, + }; - selectServiceToDelete(serviceId: number) { - this.selectedServiceId = serviceId; - this.openDeleteModal(this.deleteModal); - } - - deleteSelected(serviceId: string, ldnServicesService: LdnServicesService): void { - if (this.selectedServiceId !== null) { - ldnServicesService.delete(serviceId).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.servicesData = this.servicesData.filter(service => service.id !== serviceId); - this.ldnServicesRD$ = this.ldnServicesRD$.pipe( - map((remoteData: RemoteData>) => { - if (remoteData.hasSucceeded) { - remoteData.payload.page = remoteData.payload.page.filter(service => service.id.toString() !== serviceId); - } - return remoteData; - }) - ); - this.cdRef.detectChanges(); - this.closeModal(); - this.notificationService.success(this.translateService.get('ldn-service-delete.notification.success.title'), - this.translateService.get('ldn-service-delete.notification.success.content')); - } else { - this.notificationService.error(this.translateService.get('ldn-service-delete.notification.error.title'), - this.translateService.get('ldn-service-delete.notification.error.content')); - this.cdRef.detectChanges(); - } - }); + ldnServicesService.patch(ldnService, [patchOperation]).pipe(getFirstCompletedRemoteData()).subscribe( + (rd: RemoteData) => { + if (rd.hasSucceeded) { + ldnService.enabled = newStatus; + this.notificationService.success(this.translateService.get('ldn-enable-service.notification.success.title'), + this.translateService.get('ldn-enable-service.notification.success.content')); + } else { + ldnService.enabled = originalStatus; + this.notificationService.error(this.translateService.get('ldn-enable-service.notification.error.title'), + this.translateService.get('ldn-enable-service.notification.error.content')); } - } - - - toggleStatus(ldnService: any, ldnServicesService: LdnServicesService): void { - const newStatus = !ldnService.enabled; - const originalStatus = ldnService.enabled; - - const patchOperation: Operation = { - op: 'replace', - path: '/enabled', - value: newStatus, - }; - - ldnServicesService.patch(ldnService, [patchOperation]).pipe(getFirstCompletedRemoteData()).subscribe( - (rd: RemoteData) => { - if (rd.hasSucceeded) { - ldnService.enabled = newStatus; - this.notificationService.success(this.translateService.get('ldn-enable-service.notification.success.title'), - this.translateService.get('ldn-enable-service.notification.success.content')); - } else { - ldnService.enabled = originalStatus; - this.notificationService.error(this.translateService.get('ldn-enable-service.notification.error.title'), - this.translateService.get('ldn-enable-service.notification.error.content')); - } - } - ); - } - - + } + ); + } } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts index 6aceada3df..55b7ad8b98 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts @@ -1,31 +1,31 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; -import { LDN_SERVICE_CONSTRAINT_FILTER } from './ldn-service.resource-type'; -import { CacheableObject } from '../../../core/cache/cacheable-object.model'; -import { typedObject } from '../../../core/cache/builders/build-decorators'; -import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; -import { ResourceType } from '../../../core/shared/resource-type'; +import {autoserialize, deserialize, inheritSerialization} from 'cerialize'; +import {LDN_SERVICE_CONSTRAINT_FILTER} from './ldn-service.resource-type'; +import {CacheableObject} from '../../../core/cache/cacheable-object.model'; +import {typedObject} from '../../../core/cache/builders/build-decorators'; +import {excludeFromEquals} from '../../../core/utilities/equals.decorators'; +import {ResourceType} from '../../../core/shared/resource-type'; /** A single filter value and its properties. */ @typedObject @inheritSerialization(CacheableObject) export class Itemfilter extends CacheableObject { - static type = LDN_SERVICE_CONSTRAINT_FILTER; + static type = LDN_SERVICE_CONSTRAINT_FILTER; - @excludeFromEquals - @autoserialize - type: ResourceType; + @excludeFromEquals + @autoserialize + type: ResourceType; - @autoserialize - id: string; + @autoserialize + id: string; - @deserialize - _links: { - self: { - href: string; - }; + @deserialize + _links: { + self: { + href: string; }; + }; - get self(): string { - return this._links.self.href; - } + get self(): string { + return this._links.self.href; + } } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts index 1103056e47..295426ba87 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts @@ -1,13 +1,13 @@ -import { autoserialize } from 'cerialize'; +import {autoserialize} from 'cerialize'; /** - * notify service patterns + * A single notify service pattern and his properties */ export class NotifyServicePattern { - @autoserialize - pattern: string; - @autoserialize - constraint: string; - @autoserialize - automatic: string; + @autoserialize + pattern: string; + @autoserialize + constraint: string; + @autoserialize + automatic: string; } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts index d9e45d91a9..040e4d37b8 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts @@ -2,7 +2,7 @@ * List of services statuses */ export enum LdnServiceStatus { - UNKOWN, - DISABLED, - ENABLED, + UNKOWN, + DISABLED, + ENABLED, } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts index 69a9baf273..5121e47f69 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts @@ -1,3 +1,3 @@ -export class ldnServiceConstrain { - void: any; +export class LdnServiceConstrain { + void: any; } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts index 05a881e7e7..4fb510c032 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts @@ -4,7 +4,7 @@ * Needs to be in a separate file to prevent circular * dependencies in webpack. */ -import { ResourceType } from '../../../core/shared/resource-type'; +import {ResourceType} from '../../../core/shared/resource-type'; export const LDN_SERVICE = new ResourceType('ldnservice'); export const LDN_SERVICE_CONSTRAINT_FILTERS = new ResourceType('itemfilters'); diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts index 8631ef2451..f3e421c9a2 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts @@ -1,57 +1,60 @@ -import { ResourceType } from '../../../core/shared/resource-type'; -import { CacheableObject } from '../../../core/cache/cacheable-object.model'; -import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; -import { LDN_SERVICE } from './ldn-service.resource-type'; -import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; -import { typedObject } from '../../../core/cache/builders/build-decorators'; -import { NotifyServicePattern } from './ldn-service-patterns.model'; +import {ResourceType} from '../../../core/shared/resource-type'; +import {CacheableObject} from '../../../core/cache/cacheable-object.model'; +import {autoserialize, deserialize, deserializeAs, inheritSerialization} from 'cerialize'; +import {LDN_SERVICE} from './ldn-service.resource-type'; +import {excludeFromEquals} from '../../../core/utilities/equals.decorators'; +import {typedObject} from '../../../core/cache/builders/build-decorators'; +import {NotifyServicePattern} from './ldn-service-patterns.model'; /** An LdnService and its properties. */ @typedObject @inheritSerialization(CacheableObject) export class LdnService extends CacheableObject { - static type = LDN_SERVICE; + static type = LDN_SERVICE; - @excludeFromEquals - @autoserialize - type: ResourceType; + @excludeFromEquals + @autoserialize + type: ResourceType; - @autoserialize - id: number; + @autoserialize + id: number; - @deserializeAs('id') - uuid: string; + @deserializeAs('id') + uuid: string; - @autoserialize - name: string; + @autoserialize + name: string; - @autoserialize - description: string; + @autoserialize + description: string; - @autoserialize - url: string; + @autoserialize + url: string; - @autoserialize - enabled: boolean; + @autoserialize + score: number; - @autoserialize - ldnUrl: string; + @autoserialize + enabled: boolean; - @autoserialize - notifyServiceInboundPatterns?: NotifyServicePattern[]; + @autoserialize + ldnUrl: string; - @autoserialize - notifyServiceOutboundPatterns?: NotifyServicePattern[]; + @autoserialize + notifyServiceInboundPatterns?: NotifyServicePattern[]; - @deserialize - _links: { - self: { - href: string; - }; + @autoserialize + notifyServiceOutboundPatterns?: NotifyServicePattern[]; + + @deserialize + _links: { + self: { + href: string; }; + }; - get self(): string { - return this._links.self.href; - } + get self(): string { + return this._links.self.href; + } } diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts index d3f55c815e..c734503d95 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts @@ -2,9 +2,9 @@ * List of parameter types used for scripts */ export enum LdnServiceConstrainType { - STRING = 'String', - DATE = 'date', - BOOLEAN = 'boolean', - FILE = 'InputStream', - OUTPUT = 'OutputStream' + STRING = 'String', + DATE = 'date', + BOOLEAN = 'boolean', + FILE = 'InputStream', + OUTPUT = 'OutputStream' } diff --git a/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts index 7b0c25e832..8620bfc80c 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts @@ -1,69 +1,31 @@ export const notifyPatterns = [ - { - name: 'Acknowledge and Accept', - description: 'This pattern is used to acknowledge and accept a request (offer). It implies an intention to act on the request.', - category: 'Acknowledgements' - }, - { - name: 'Acknowledge and Reject', - description: 'This pattern is used to acknowledge and reject a request (offer). It signifies no further action regarding the request.', - category: 'Acknowledgements' - }, - { - name: 'Acknowledge and Tentatively Accept', - description: 'This pattern is used to acknowledge and tentatively accept a request (offer). It implies an intention to act, which may change.', - category: 'Acknowledgements' - }, - { - name: 'Acknowledge and Tentatively Reject', - description: 'This pattern is used to acknowledge and tentatively reject a request (offer). It signifies no further action, subject to change.', - category: 'Acknowledgements' - }, - { - name: 'Announce Endorsement', - description: 'This pattern is used to announce the existence of an endorsement, referencing the endorsed resource.', - category: 'Announcements' - }, - { - name: 'Announce Ingest', - description: 'This pattern is used to announce that a resource has been ingested.', - category: 'Announcements' - }, - { - name: 'Announce Relationship', - description: 'This pattern is used to announce a relationship between two resources.', - category: 'Announcements' - }, - { - name: 'Announce Review', - description: 'This pattern is used to announce the existence of a review, referencing the reviewed resource.', - category: 'Announcements' - }, - { - name: 'Announce Service Result', - description: 'This pattern is used to announce the existence of a "service result", referencing the relevant resource.', - category: 'Announcements' - }, - { - name: 'Request Endorsement', - description: 'This pattern is used to request endorsement of a resource owned by the origin system.', - category: 'Requests' - }, - { - name: 'Request Ingest', - description: 'This pattern is used to request that the target system ingest a resource.', - category: 'Requests' - }, - { - name: 'Request Review', - description: 'This pattern is used to request a review of a resource owned by the origin system.', - category: 'Requests' - }, - { - name: 'Undo Offer', - description: 'This pattern is used to undo (retract) an offer previously made.', - category: 'Undo' - } + + 'ack-accept', + + 'ack-reject', + + 'ack-tentative-accept', + + 'ack-tentative-reject', + + 'announce-endorsement', + + 'announce-ingest', + + 'announce-relationship', + + 'announce-review', + + 'announce-service-result', + + 'request-endorsement', + + 'request-ingest', + + 'request-review', + + 'undo-offer', + ]; diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts deleted file mode 100644 index ab4f78dc6b..0000000000 --- a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { LdnDirectoryService } from './ldn-directory.service'; - -describe('LdnDirectoryService', () => { - let service: LdnDirectoryService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(LdnDirectoryService); - }); - - it('should be created', () => { - // @ts-ignore - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts deleted file mode 100644 index 92446b4677..0000000000 --- a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-directory.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { map, Observable } from 'rxjs'; -import { LdnServicesService } from "../ldn-services-data/ldn-services-data.service"; - -@Injectable({ - providedIn: 'root', -}) -export class LdnDirectoryService { - private itemFilterEndpoint = 'http://localhost:8080/server/api/config/itemfilters'; - - - constructor(private http: HttpClient, - private ldnServicesService: LdnServicesService) { - } - - public getItemFilters(): Observable { - - return this.ldnServicesService.findAll().pipe( - map((servicesData) => { - return servicesData; - }) - ); - } - -} - - - diff --git a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts deleted file mode 100644 index 922b63a871..0000000000 --- a/src/app/admin/admin-ldn-services/ldn-services-services/ldn-service-bulk-delete.service.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { LdnServicesBulkDeleteService } from './ldn-service-bulk-delete.service'; - -describe('LdnServiceBulkDeleteService', () => { - let service: LdnServicesBulkDeleteService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(LdnServicesBulkDeleteService); - }); - - it('should be created', () => { - // @ts-ignore - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts index 60fa679777..b943369928 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -49,6 +49,21 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon showBreadcrumbsFluid: false } }, + { + canActivate: [ AuthenticatedGuard ], + path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, + component: AdminQualityAssuranceTopicsPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: I18nBreadcrumbResolver, + openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver + }, + data: { + title: 'admin.quality-assurance.page.title', + breadcrumbKey: 'admin.quality-assurance', + showBreadcrumbsFluid: false + } + }, { canActivate: [ AuthenticatedGuard ], path: `${QUALITY_ASSURANCE_EDIT_PATH}`, diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.html b/src/app/core/coar-notify/notify-info/notify-info.component.html index 03ed39a826..640cf27386 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.component.html +++ b/src/app/core/coar-notify/notify-info/notify-info.component.html @@ -7,7 +7,7 @@

{{ 'coar-notify-support.ldn-inbox.title' | translate }}

-

+

{{ 'coar-notify-support.message-moderation.title' | translate }}

diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts index eae3a3e3d6..e14fba61c1 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts @@ -1,14 +1,23 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NotifyInfoComponent } from './notify-info.component'; +import { NotifyInfoService } from './notify-info.service'; +import { TranslateModule } from '@ngx-translate/core'; describe('NotifyInfoComponent', () => { let component: NotifyInfoComponent; let fixture: ComponentFixture; + let notifyInfoServiceSpy: any; beforeEach(async () => { + notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['getCoarLdnLocalInboxUrls']); + await TestBed.configureTestingModule({ - declarations: [ NotifyInfoComponent ] + imports: [TranslateModule.forRoot()], + declarations: [ NotifyInfoComponent ], + providers: [ + { provide: NotifyInfoService, useValue: notifyInfoServiceSpy } + ] }) .compileComponents(); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.ts b/src/app/core/coar-notify/notify-info/notify-info.component.ts index 1abb54c11e..7de2cf538a 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.component.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.component.ts @@ -1,26 +1,24 @@ import { Component, OnInit } from '@angular/core'; import { NotifyInfoService } from './notify-info.service'; - +import { Observable, of } from 'rxjs'; @Component({ - selector: 'ds-notify-info', - templateUrl: './notify-info.component.html', - styleUrls: ['./notify-info.component.scss'] + selector: 'ds-notify-info', + templateUrl: './notify-info.component.html', + styleUrls: ['./notify-info.component.scss'], }) +/** + * Component for displaying COAR notification information. + */ export class NotifyInfoComponent implements OnInit { - coarLdnEnabled: boolean; - coarRestApiUrl: string; + /** + * Observable containing the COAR REST INBOX API URLs. + */ + coarRestApiUrl: Observable = of([]); - constructor( - public notifyInfoService: NotifyInfoService, - ) { - } + constructor(private notifyInfoService: NotifyInfoService) {} - ngOnInit() { - this.coarRestApiUrl = this.notifyInfoService.getCoarLdnRestApiUrl(); - - this.notifyInfoService.isCoarConfigEnabled().subscribe(value => { - this.coarLdnEnabled = value; - }); - } + ngOnInit() { + this.coarRestApiUrl = this.notifyInfoService.getCoarLdnLocalInboxUrls(); + } } diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts index 789140ebe4..81ac0db8d8 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts @@ -1,16 +1,49 @@ import { TestBed } from '@angular/core/testing'; import { NotifyInfoGuard } from './notify-info.guard'; +import { Router } from '@angular/router'; +import { NotifyInfoService } from './notify-info.service'; +import { of } from 'rxjs'; describe('NotifyInfoGuard', () => { let guard: NotifyInfoGuard; + let notifyInfoServiceSpy: any; + let router: any; beforeEach(() => { - TestBed.configureTestingModule({}); + notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); + router = jasmine.createSpyObj('Router', ['parseUrl']); + TestBed.configureTestingModule({ + providers: [ + NotifyInfoGuard, + { provide: NotifyInfoService, useValue: notifyInfoServiceSpy}, + { provide: Router, useValue: router} + ] + }); guard = TestBed.inject(NotifyInfoGuard); }); it('should be created', () => { expect(guard).toBeTruthy(); }); + + it('should return true if COAR config is enabled', (done) => { + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); + + guard.canActivate(null, null).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it('should call parseUrl method of Router if COAR config is not enabled', (done) => { + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); + router.parseUrl.and.returnValue(of('/404')); + + guard.canActivate(null, null).subscribe(() => { + expect(router.parseUrl).toHaveBeenCalledWith('/404'); + done(); + }); + }); + }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.ts index 4da9a42f21..7af0821618 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.ts @@ -16,7 +16,7 @@ export class NotifyInfoGuard implements CanActivate { canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot - ): Observable | Promise | boolean | UrlTree { + ): Observable { return this.notifyInfoService.isCoarConfigEnabled().pipe( map(coarLdnEnabled => { if (coarLdnEnabled) { diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts index b32d590cd6..092dab1655 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts @@ -1,16 +1,50 @@ import { TestBed } from '@angular/core/testing'; import { NotifyInfoService } from './notify-info.service'; +import { ConfigurationDataService } from '../../data/configuration-data.service'; +import { of } from 'rxjs'; describe('NotifyInfoService', () => { let service: NotifyInfoService; + let configurationDataService: any; beforeEach(() => { - TestBed.configureTestingModule({}); + configurationDataService = { + findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(of({})), + }; + TestBed.configureTestingModule({ + providers: [ + NotifyInfoService, + { provide: ConfigurationDataService, useValue: configurationDataService }, + ] + }); service = TestBed.inject(NotifyInfoService); + configurationDataService = TestBed.inject(ConfigurationDataService); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should retrieve and map coar configuration', () => { + const mockResponse = { payload: { values: ['true'] } }; + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + + service.isCoarConfigEnabled().subscribe((result) => { + expect(result).toBe(true); + }); + }); + + it('should retrieve and map LDN local inbox URLs', () => { + const mockResponse = { values: ['inbox1', 'inbox2'] }; + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + + service.getCoarLdnLocalInboxUrls().subscribe((result) => { + expect(result).toEqual(['inbox1', 'inbox2']); + }); + }); + + it('should return the inbox relation link', () => { + expect(service.getInboxRelationLink()).toBe('http://www.w3.org/ns/ldp#inbox'); + }); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.ts b/src/app/core/coar-notify/notify-info/notify-info.service.ts index a88181a9ab..8690a07565 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.ts @@ -1,14 +1,19 @@ import { Injectable } from '@angular/core'; -import { getFirstSucceededRemoteData } from '../../shared/operators'; +import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators'; import { ConfigurationDataService } from '../../data/configuration-data.service'; import { map, Observable } from 'rxjs'; -import { DefaultAppConfig } from '../../../../config/default-app-config'; +import { ConfigurationProperty } from '../../shared/configuration-property.model'; @Injectable({ providedIn: 'root' }) export class NotifyInfoService { + /** + * The relation link for the inbox + */ + private _inboxRelationLink = 'http://www.w3.org/ns/ldp#inbox'; + constructor( private configService: ConfigurationDataService, ) {} @@ -24,15 +29,25 @@ export class NotifyInfoService { ); } - getCoarLdnRestApiUrl(): string { - const appConfig = new DefaultAppConfig(); - const restConfig = appConfig.rest; + /** + * Get the url of the local inbox from the REST configuration + * @returns the url of the local inbox + */ + getCoarLdnLocalInboxUrls(): Observable { + return this.configService.findByPropertyName('ldn.notify.inbox').pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map((response: ConfigurationProperty) => { + return response.values; + }) + ); + } - const ssl = restConfig.ssl; - const host = restConfig.host; - const port = restConfig.port; - const namespace = restConfig.nameSpace; - - return `${ssl ? 'https' : 'http'}://${host}:${port}${namespace}`; + /** + * Method to get the relation link for the inbox + * @returns the relation link for the inbox + */ + getInboxRelationLink(): string { + return this._inboxRelationLink; } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 88da00f02c..d0f2dbbbaf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -190,7 +190,11 @@ import { SuggestionSource } from './suggestion-notifications/reciter-suggestions import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; -import { Itemfilter } from "../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters"; +import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; +import { + CoarNotifyConfigDataService +} from '../submission/sections/section-coar-notify/coar-notify-config-data.service'; +import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -315,7 +319,8 @@ const PROVIDERS = [ OrcidHistoryDataService, SupervisionOrderDataService, LdnServicesService, - LdnItemfiltersService + LdnItemfiltersService, + CoarNotifyConfigDataService ]; /** @@ -398,7 +403,8 @@ export const models = SuggestionTarget, SuggestionSource, LdnService, - Itemfilter + Itemfilter, + SubmissionCoarNotifyConfig ]; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 1433ebacce..43b60f874d 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -25,7 +25,6 @@ import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheEntry } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index a3ccd49dac..dd19c3fb8b 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -26,3 +26,5 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionSherpaPoliciesObject | WorkspaceitemSectionIdentifiersObject | string; + + diff --git a/src/app/core/submission/workspaceitem-data.service.spec.ts b/src/app/core/submission/workspaceitem-data.service.spec.ts index e766a6a039..2a3a3d343d 100644 --- a/src/app/core/submission/workspaceitem-data.service.spec.ts +++ b/src/app/core/submission/workspaceitem-data.service.spec.ts @@ -19,7 +19,6 @@ import { Item } from '../shared/item.model'; import { WorkspaceItem } from './models/workspaceitem.model'; import { RequestEntry } from '../data/request-entry.model'; import { CoreState } from '../core-state.model'; -import { testSearchDataImplementation } from '../data/base/search-data.spec'; import { testDeleteDataImplementation } from '../data/base/delete-data.spec'; describe('WorkspaceitemDataService test', () => { @@ -84,17 +83,19 @@ describe('WorkspaceitemDataService test', () => { function initTestService() { hrefOnlyDataService = getMockHrefOnlyDataService(); return new WorkspaceitemDataService( + comparator, + halService, + http, + notificationsService, requestService, rdbService, objectCache, - halService, - notificationsService, + store, ); } describe('composition', () => { - const initService = () => new WorkspaceitemDataService(null, null, null, null, null); - testSearchDataImplementation(initService); + const initService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null); testDeleteDataImplementation(initService); }); @@ -126,7 +127,7 @@ describe('WorkspaceitemDataService test', () => { service = initTestService(); spyOn((service as any), 'findByHref').and.callThrough(); - spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$); + spyOn((service as any), 'getIDHref').and.callThrough(); }); afterEach(() => { @@ -138,7 +139,7 @@ describe('WorkspaceitemDataService test', () => { scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo)); scheduler.flush(); - expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true); + expect((service as any).findByHref).toHaveBeenCalled(); }); it('should return a RemoteData for the search', () => { diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 8a036f6443..f430dd43ea 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -50,6 +50,20 @@ export class WorkspaceitemDataService extends IdentifiableDataService> { return this.deleteData.delete(objectId, copyVirtualMetadata); } + + /** + * Delete an existing object on the server + * @param href The self link of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc + * Only emits once all request related to the DSO has been invalidated. + */ + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + /** * Return the WorkspaceItem object found through the UUID of an item * diff --git a/src/app/core/suggestion-notifications/qa/events/quality-assurance-event-data.service.ts b/src/app/core/suggestion-notifications/qa/events/quality-assurance-event-data.service.ts index 7f7e68afaa..6c333cc6f5 100644 --- a/src/app/core/suggestion-notifications/qa/events/quality-assurance-event-data.service.ts +++ b/src/app/core/suggestion-notifications/qa/events/quality-assurance-event-data.service.ts @@ -84,6 +84,16 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService[]): Observable>> { + return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow); + } + /** * Clear findByTopic requests from cache */ diff --git a/src/app/core/suggestion-notifications/qa/source/quality-assurance-source-data.service.ts b/src/app/core/suggestion-notifications/qa/source/quality-assurance-source-data.service.ts index 03a5da2e8c..130e7261d1 100644 --- a/src/app/core/suggestion-notifications/qa/source/quality-assurance-source-data.service.ts +++ b/src/app/core/suggestion-notifications/qa/source/quality-assurance-source-data.service.ts @@ -16,6 +16,7 @@ import { PaginatedList } from '../../../data/paginated-list.model'; import { FindListOptions } from '../../../data/find-list-options.model'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; +import { SearchData, SearchDataImpl } from 'src/app/core/data/base/search-data'; /** * The service handling all Quality Assurance source REST requests. @@ -25,6 +26,9 @@ import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; export class QualityAssuranceSourceDataService extends IdentifiableDataService { private findAllData: FindAllData; + private searchAllData: SearchData; + + private searchByTargetMethod = 'byTarget'; /** * Initialize service variables @@ -43,6 +47,7 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService[]): Observable> { return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + /** + * Retrieves a paginated list of QualityAssuranceSourceObject objects that are associated with a given target object. + * @param options The options for the search query. + * @param useCachedVersionIfAvailable Whether to use a cached version of the data if available. + * @param reRequestOnStale Whether to re-request the data if the cached version is stale. + * @param linksToFollow The links to follow to retrieve the data. + * @returns An observable that emits a RemoteData object containing the paginated list of QualityAssuranceSourceObject objects. + */ + public getSourcesByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchAllData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } diff --git a/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.spec.ts b/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.spec.ts index 638ee3fa62..277da6e4ba 100644 --- a/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.spec.ts +++ b/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.spec.ts @@ -80,22 +80,53 @@ describe('QualityAssuranceTopicDataService', () => { notificationsService ); - spyOn((service as any).findAllData, 'findAll').and.callThrough(); + spyOn((service as any).searchData, 'searchBy').and.callThrough(); spyOn((service as any), 'findById').and.callThrough(); }); - describe('getTopics', () => { - it('should call findListByHref', (done) => { - service.getTopics().subscribe( - (res) => { - expect((service as any).findAllData.findAll).toHaveBeenCalledWith({}, true, true); - } + describe('searchTopicsByTarget', () => { + it('should call searchData.searchBy with the correct parameters', () => { + const options = { elementsPerPage: 10 }; + const useCachedVersionIfAvailable = true; + const reRequestOnStale = true; + + service.searchTopicsByTarget(options, useCachedVersionIfAvailable, reRequestOnStale); + + expect((service as any).searchData.searchBy).toHaveBeenCalledWith( + 'byTarget', + options, + useCachedVersionIfAvailable, + reRequestOnStale ); - done(); }); it('should return a RemoteData> for the object with the given URL', () => { - const result = service.getTopics(); + const result = service.searchTopicsByTarget(); + const expected = cold('(a)', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchTopicsBySource', () => { + it('should call searchData.searchBy with the correct parameters', () => { + const options = { elementsPerPage: 10 }; + const useCachedVersionIfAvailable = true; + const reRequestOnStale = true; + + service.searchTopicsBySource(options, useCachedVersionIfAvailable, reRequestOnStale); + + expect((service as any).searchData.searchBy).toHaveBeenCalledWith( + 'bySource', + options, + useCachedVersionIfAvailable, + reRequestOnStale, + ); + }); + + it('should return a RemoteData> for the object with the given URL', () => { + const result = service.searchTopicsBySource(); const expected = cold('(a)', { a: paginatedListRD }); @@ -121,5 +152,4 @@ describe('QualityAssuranceTopicDataService', () => { expect(result).toBeObservable(expected); }); }); - }); diff --git a/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.ts b/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.ts index 2bf5195bf1..86c8f5eeaa 100644 --- a/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.ts +++ b/src/app/core/suggestion-notifications/qa/topics/quality-assurance-topic-data.service.ts @@ -15,6 +15,7 @@ import { FindListOptions } from '../../../data/find-list-options.model'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { dataService } from '../../../data/base/data-service.decorator'; import { QUALITY_ASSURANCE_TOPIC_OBJECT } from '../models/quality-assurance-topic-object.resource-type'; +import { SearchData, SearchDataImpl } from '../../../../core/data/base/search-data'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; /** @@ -25,6 +26,10 @@ import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; export class QualityAssuranceTopicDataService extends IdentifiableDataService { private findAllData: FindAllData; + private searchData: SearchData; + + private searchByTargetMethod = 'byTarget'; + private searchBySourceMethod = 'bySource'; /** * Initialize service variables @@ -43,23 +48,31 @@ export class QualityAssuranceTopicDataService extends IdentifiableDataService>> - * The list of Quality Assurance topics. + * Search for Quality Assurance topics. + * @param options The search options. + * @param useCachedVersionIfAvailable Whether to use cached version if available. + * @param reRequestOnStale Whether to re-request on stale. + * @param linksToFollow The links to follow. + * @returns An observable of remote data containing a paginated list of Quality Assurance topics. */ - public getTopics(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + public searchTopicsByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Searches for quality assurance topics by source. + * @param options The search options. + * @param useCachedVersionIfAvailable Whether to use a cached version if available. + * @param reRequestOnStale Whether to re-request the data if it's stale. + * @param linksToFollow The links to follow. + * @returns An observable of the remote data containing the paginated list of quality assurance topics. + */ + public searchTopicsBySource(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(this.searchBySourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/footer/footer.component.spec.ts b/src/app/footer/footer.component.spec.ts index 9f0250edc4..010d4e22c2 100644 --- a/src/app/footer/footer.component.spec.ts +++ b/src/app/footer/footer.component.spec.ts @@ -1,5 +1,5 @@ // ... test imports -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, inject, TestBed,waitForAsync } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; @@ -9,6 +9,7 @@ import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { StoreModule } from '@ngrx/store'; +import { of } from 'rxjs'; // Load the implementations that should be tested import { FooterComponent } from './footer.component'; @@ -17,15 +18,21 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { storeModuleConfig } from '../app.reducer'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub'; +import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service'; +import { environment } from 'src/environments/environment'; + let comp: FooterComponent; +let compAny: any; let fixture: ComponentFixture; let de: DebugElement; let el: HTMLElement; -describe('Footer component', () => { +let notifyInfoService = { + isCoarConfigEnabled: () => of(true) +}; - // waitForAsync beforeEach +describe('Footer component', () => { beforeEach(waitForAsync(() => { return TestBed.configureTestingModule({ imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ @@ -38,6 +45,7 @@ describe('Footer component', () => { providers: [ FooterComponent, { provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub }, + { provide: NotifyInfoService, useValue: notifyInfoService } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -46,9 +54,8 @@ describe('Footer component', () => { // synchronous beforeEach beforeEach(() => { fixture = TestBed.createComponent(FooterComponent); - - comp = fixture.componentInstance; // component test instance - + comp = fixture.componentInstance; + compAny = comp as any; // query for the title

by CSS element selector de = fixture.debugElement.query(By.css('p')); el = de.nativeElement; @@ -59,4 +66,56 @@ describe('Footer component', () => { expect(app).toBeTruthy(); })); + + it('should set showPrivacyPolicy to the value of environment.info.enablePrivacyStatement', () => { + expect(comp.showPrivacyPolicy).toBe(environment.info.enablePrivacyStatement); + }); + + it('should set showEndUserAgreement to the value of environment.info.enableEndUserAgreement', () => { + expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement); + }); + + describe('showCookieSettings', () => { + it('should call cookies.showSettings() if cookies is defined', () => { + const cookies = jasmine.createSpyObj('cookies', ['showSettings']); + compAny.cookies = cookies; + comp.showCookieSettings(); + expect(cookies.showSettings).toHaveBeenCalled(); + }); + + it('should not call cookies.showSettings() if cookies is undefined', () => { + compAny.cookies = undefined; + expect(() => comp.showCookieSettings()).not.toThrow(); + }); + + it('should return false', () => { + expect(comp.showCookieSettings()).toBeFalse(); + }); + }); + + describe('when coarLdnEnabled is true', () => { + beforeEach(() => { + spyOn(notifyInfoService, 'isCoarConfigEnabled').and.returnValue(of(true)); + fixture.detectChanges(); + }); + + it('should set coarLdnEnabled based on notifyInfoService', () => { + expect(comp.coarLdnEnabled).toBeTruthy(); + // Check if COAR Notify section is rendered + const notifySection = fixture.debugElement.query(By.css('.notify-enabled')); + expect(notifySection).toBeTruthy(); + }); + + it('should redirect to info/coar-notify-support', () => { + // Check if the link to the COAR Notify support page is present + const routerLink = fixture.debugElement.query(By.css('a[routerLink="info/coar-notify-support"].coar-notify-support-route')); + expect(routerLink).toBeTruthy(); + }); + + it('should have an img tag with the class "n-coar" when coarLdnEnabled is true', fakeAsync(() => { + // Check if the img tag with the class "n-coar" is present + const imgTag = fixture.debugElement.query(By.css('.notify-enabled img.n-coar')); + expect(imgTag).toBeTruthy(); + })); + }); }); diff --git a/src/app/home-page/home-page.component.ts b/src/app/home-page/home-page.component.ts index c151cbbb16..585fd06fe6 100644 --- a/src/app/home-page/home-page.component.ts +++ b/src/app/home-page/home-page.component.ts @@ -1,22 +1,50 @@ -import { Component, OnInit } from '@angular/core'; -import { map } from 'rxjs/operators'; +import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { map, switchMap } from 'rxjs/operators'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { Site } from '../core/shared/site.model'; import { environment } from '../../environments/environment'; +import { isPlatformServer } from '@angular/common'; +import { ServerResponseService } from '../core/services/server-response.service'; +import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service'; +import { LinkDefinition, LinkHeadService } from '../core/services/link-head.service'; +import { isNotEmpty } from '../shared/empty.util'; + @Component({ selector: 'ds-home-page', styleUrls: ['./home-page.component.scss'], templateUrl: './home-page.component.html' }) -export class HomePageComponent implements OnInit { +export class HomePageComponent implements OnInit, OnDestroy { site$: Observable; recentSubmissionspageSize: number; + /** + * An array of LinkDefinition objects representing inbox links for the home page. + */ + inboxLinks: LinkDefinition[] = []; + constructor( private route: ActivatedRoute, + private responseService: ServerResponseService, + private notifyInfoService: NotifyInfoService, + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) private platformId: string ) { this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize; + // Get COAR REST API URLs from REST configuration + // only if COAR configuration is enabled + this.notifyInfoService.isCoarConfigEnabled().pipe( + switchMap((coarLdnEnabled: boolean) => { + if (coarLdnEnabled) { + return this.notifyInfoService.getCoarLdnLocalInboxUrls(); + } + }) + ).subscribe((coarRestApiUrls: string[]) => { + if (coarRestApiUrls.length > 0) { + this.initPageLinks(coarRestApiUrls); + } + }); } ngOnInit(): void { @@ -24,4 +52,38 @@ export class HomePageComponent implements OnInit { map((data) => data.site as Site), ); } + + /** + * Initializes page links for COAR REST API URLs. + * @param coarRestApiUrls An array of COAR REST API URLs. + */ + private initPageLinks(coarRestApiUrls: string[]): void { + const rel = this.notifyInfoService.getInboxRelationLink(); + let links = ''; + coarRestApiUrls.forEach((coarRestApiUrl: string) => { + // Add link to head + let tag: LinkDefinition = { + href: coarRestApiUrl, + rel: rel + }; + this.inboxLinks.push(tag); + this.linkHeadService.addTag(tag); + + links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`; + }); + + if (isPlatformServer(this.platformId)) { + // Add link to response header + this.responseService.setHeader('Link', links); + } + } + + /** + * It removes the inbox links from the head of the html. + */ + ngOnDestroy(): void { + this.inboxLinks.forEach((link: LinkDefinition) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }); + } } diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 9fc078c2cd..c09c3177e3 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -23,6 +23,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -61,6 +62,7 @@ describe('FullItemPageComponent', () => { let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; let linkHeadService: jasmine.SpyObj; + let notifyInfoService: jasmine.SpyObj; const mocklink = { href: 'http://test.org', @@ -105,6 +107,12 @@ describe('FullItemPageComponent', () => { removeTag: jasmine.createSpy('removeTag'), }); + notifyInfoService = jasmine.createSpyObj('NotifyInfoService', { + isCoarConfigEnabled: observableOf(true), + getCoarLdnLocalInboxUrls: observableOf(['http://test.org']), + getInboxRelationLink: observableOf('http://test.org'), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -122,6 +130,7 @@ describe('FullItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: notifyInfoService }, { provide: PLATFORM_ID, useValue: 'server' } ], schemas: [NO_ERRORS_SCHEMA] @@ -178,7 +187,7 @@ describe('FullItemPageComponent', () => { it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(3); }); }); describe('when the item is withdrawn and the user is not an admin', () => { @@ -207,7 +216,7 @@ describe('FullItemPageComponent', () => { it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(3); }); }); @@ -224,7 +233,7 @@ describe('FullItemPageComponent', () => { it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(3); }); }); }); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 31dd2c5fc2..09238c30ab 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -19,6 +19,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; /** * This component renders a full item page. @@ -55,9 +56,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, protected linkHeadService: LinkHeadService, + protected notifyInfoService: NotifyInfoService, @Inject(PLATFORM_ID) protected platformId: string, ) { - super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId); + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, notifyInfoService, platformId); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index a8d41d1535..5aa1b6e508 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -60,6 +60,7 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component import { ThemedFullFileSectionComponent } from './full/field-components/file-section/themed-full-file-section.component'; +import { QaEventNotificationComponent } from './simple/qa-event-notification/qa-event-notification.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -103,6 +104,7 @@ const DECLARATIONS = [ ItemAlertsComponent, ThemedItemAlertsComponent, BitstreamRequestACopyPageComponent, + QaEventNotificationComponent ]; @NgModule({ diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index cc9983bb35..37a5e0c4cb 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -2,6 +2,7 @@

+ diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index b3202108f4..433b950cee 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -26,6 +26,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; import { SignpostingLink } from '../../core/data/signposting-links.model'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -62,6 +63,7 @@ describe('ItemPageComponent', () => { let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; let linkHeadService: jasmine.SpyObj; + let notifyInfoService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -73,6 +75,8 @@ describe('ItemPageComponent', () => { data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }) }); + const getCoarLdnLocalInboxUrls = ['http://InboxUrls.org', 'http://InboxUrls2.org']; + beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), @@ -94,6 +98,12 @@ describe('ItemPageComponent', () => { removeTag: jasmine.createSpy('removeTag'), }); + notifyInfoService = jasmine.createSpyObj('NotifyInfoService', { + getInboxRelationLink: 'http://www.w3.org/ns/ldp#inbox', + isCoarConfigEnabled: observableOf(true), + getCoarLdnLocalInboxUrls: observableOf(getCoarLdnLocalInboxUrls), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -112,6 +122,7 @@ describe('ItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: NotifyInfoService, useValue: notifyInfoService}, { provide: PLATFORM_ID, useValue: 'server' }, ], @@ -166,7 +177,7 @@ describe('ItemPageComponent', () => { it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(4); }); @@ -175,7 +186,7 @@ describe('ItemPageComponent', () => { expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]); // Check if linkHeadService.addTag() was called with the correct arguments - expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length + getCoarLdnLocalInboxUrls.length); let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition; expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); expected = { @@ -186,8 +197,7 @@ describe('ItemPageComponent', () => { }); it('should set Link header on the server', () => { - - expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" '); + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" , ; rel="http://www.w3.org/ns/ldp#inbox", ; rel="http://www.w3.org/ns/ldp#inbox"'); }); }); @@ -215,9 +225,9 @@ describe('ItemPageComponent', () => { expect(objectLoader.nativeElement).toBeDefined(); }); - it('should add the signposting links', () => { + it('should add the signposti`ng links`', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(4); }); }); @@ -234,7 +244,7 @@ describe('ItemPageComponent', () => { it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); - expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(4); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index b9be6bebfb..a057e99715 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM import { ActivatedRoute, Router } from '@angular/router'; import { isPlatformServer } from '@angular/common'; -import { Observable } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { Observable, combineLatest } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -21,6 +21,7 @@ import { SignpostingDataService } from '../../core/data/signposting-data.service import { SignpostingLink } from '../../core/data/signposting-links.model'; import { isNotEmpty } from '../../shared/empty.util'; import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; +import { NotifyInfoService } from 'src/app/core/coar-notify/notify-info/notify-info.service'; /** * This component renders a simple item page. @@ -32,7 +33,7 @@ import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.s styleUrls: ['./item-page.component.scss'], templateUrl: './item-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeInOut] + animations: [fadeInOut], }) export class ItemPageComponent implements OnInit, OnDestroy { @@ -68,6 +69,13 @@ export class ItemPageComponent implements OnInit, OnDestroy { */ signpostingLinks: SignpostingLink[] = []; + /** + * An array of LinkDefinition objects representing inbox links for the item page. + */ + inboxTags: LinkDefinition[] = []; + + coarRestApiUrls: string[] = []; + constructor( protected route: ActivatedRoute, protected router: Router, @@ -77,6 +85,7 @@ export class ItemPageComponent implements OnInit, OnDestroy { protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, protected linkHeadService: LinkHeadService, + protected notifyInfoService: NotifyInfoService, @Inject(PLATFORM_ID) protected platformId: string ) { this.initPageLinks(); @@ -106,7 +115,8 @@ export class ItemPageComponent implements OnInit, OnDestroy { */ private initPageLinks(): void { this.route.params.subscribe(params => { - this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + combineLatest([this.signpostingDataService.getLinks(params.id).pipe(take(1)), this.getCoarLdnLocalInboxUrls()]) + .subscribe(([signpostingLinks, coarRestApiUrls]) => { let links = ''; this.signpostingLinks = signpostingLinks; @@ -124,6 +134,11 @@ export class ItemPageComponent implements OnInit, OnDestroy { this.linkHeadService.addTag(tag); }); + if (coarRestApiUrls.length > 0) { + let inboxLinks = this.initPageInboxLinks(coarRestApiUrls); + links = links + (isNotEmpty(links) ? ', ' : '') + inboxLinks; + } + if (isPlatformServer(this.platformId)) { this.responseService.setHeader('Link', links); } @@ -131,9 +146,49 @@ export class ItemPageComponent implements OnInit, OnDestroy { }); } + /** + * Sets the COAR LDN local inbox URL if COAR configuration is enabled. + * If the COAR LDN local inbox URL is retrieved successfully, initializes the page inbox links. + */ + private getCoarLdnLocalInboxUrls(): Observable { + return this.notifyInfoService.isCoarConfigEnabled().pipe( + switchMap((coarLdnEnabled: boolean) => { + if (coarLdnEnabled) { + return this.notifyInfoService.getCoarLdnLocalInboxUrls(); + } + }) + ); + } + + /** + * Initializes the page inbox links. + * @param coarRestApiUrls - An array of COAR REST API URLs. + */ + private initPageInboxLinks(coarRestApiUrls: string[]): string { + const rel = this.notifyInfoService.getInboxRelationLink(); + let links = ''; + + coarRestApiUrls.forEach((coarRestApiUrl: string) => { + // Add link to head + let tag: LinkDefinition = { + href: coarRestApiUrl, + rel: rel + }; + this.inboxTags.push(tag); + this.linkHeadService.addTag(tag); + + links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`; + }); + + return links; + } + ngOnDestroy(): void { this.signpostingLinks.forEach((link: SignpostingLink) => { this.linkHeadService.removeTag(`href='${link.href}'`); }); + this.inboxTags.forEach((link: LinkDefinition) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }); } } diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 3749f63964..3014fcb302 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -84,6 +84,18 @@ [label]="'item.page.uri'"> + + + + + +
{{"item.page.link.full" | translate}} diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html new file mode 100644 index 0000000000..7f9e7fbd4e --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html @@ -0,0 +1,14 @@ + + +
+ +
+
{{'item.qa-event-notification.check.notification-info' | translate : {num: + source.totalEvents } }}
+ +
+
+
+
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss new file mode 100644 index 0000000000..ab33b46fca --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss @@ -0,0 +1,8 @@ + +.source-logo { + max-height: var(--ds-header-logo-height); +} + +.sections-gap { + gap: 1rem; +} diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts new file mode 100644 index 0000000000..4c8a43a1ab --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QaEventNotificationComponent } from './qa-event-notification.component'; +import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { SplitPipe } from '../../../shared/utils/split.pipe'; +import { RequestService } from '../../../core/data/request.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { provideMockStore } from '@ngrx/store/testing'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; + +describe('QaEventNotificationComponent', () => { + let component: QaEventNotificationComponent; + let fixture: ComponentFixture; + + let qualityAssuranceSourceDataServiceStub: any; + + const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()])); + const item = Object.assign({ uuid: '1234' }); + + beforeEach(async () => { + qualityAssuranceSourceDataServiceStub = { + getSourcesByTarget: () => obj + }; + await TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot()], + declarations: [QaEventNotificationComponent, SplitPipe], + providers: [ + { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }, + { provide: RequestService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test')}, + ObjectCacheService, + RemoteDataBuildService, + provideMockStore({}) + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + + fixture = TestBed.createComponent(QaEventNotificationComponent); + component = fixture.componentInstance; + component.item = item; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts new file mode 100644 index 0000000000..30393367a3 --- /dev/null +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts @@ -0,0 +1,53 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; +import { Observable } from 'rxjs'; +import { AlertType } from '../../../shared/alert/aletr-type'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; +import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service'; +import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model'; + +@Component({ + selector: 'ds-qa-event-notification', + templateUrl: './qa-event-notification.component.html', + styleUrls: ['./qa-event-notification.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [QualityAssuranceSourceDataService] +}) +/** + * Component for displaying quality assurance event notifications for an item. + */ +export class QaEventNotificationComponent { + + /** + * The item to display quality assurance event notifications for. + */ + @Input() item: Item; + + /** + * The type of alert to display for the notification. + */ + AlertTypeInfo = AlertType.Info; + + constructor( + private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService, + ) { } + + /** + * Returns an Observable of QualityAssuranceSourceObject[] for the current item. + * @returns An Observable of QualityAssuranceSourceObject[] for the current item. + * Note: sourceId is composed as: id: "sourceName:" + */ + getQualityAssuranceSources$(): Observable { + const findListTopicOptions: FindListOptions = { + searchParams: [new RequestParam('target', this.item.uuid)] + }; + return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions) + .pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload(), + ); + } +} diff --git a/src/app/my-dspace-page/my-dspace-page.component.html b/src/app/my-dspace-page/my-dspace-page.component.html index c5e49b0cec..70bcf1b7bc 100644 --- a/src/app/my-dspace-page/my-dspace-page.component.html +++ b/src/app/my-dspace-page/my-dspace-page.component.html @@ -1,4 +1,5 @@
+
diff --git a/src/app/my-dspace-page/my-dspace-page.module.ts b/src/app/my-dspace-page/my-dspace-page.module.ts index b75806cec7..60726bacc4 100644 --- a/src/app/my-dspace-page/my-dspace-page.module.ts +++ b/src/app/my-dspace-page/my-dspace-page.module.ts @@ -16,6 +16,7 @@ import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; import { SearchModule } from '../shared/search/search.module'; import { UploadModule } from '../shared/upload/upload.module'; import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.module'; +import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component'; const DECLARATIONS = [ MyDSpacePageComponent, @@ -23,7 +24,8 @@ const DECLARATIONS = [ MyDSpaceNewSubmissionComponent, CollectionSelectorComponent, MyDSpaceNewSubmissionDropdownComponent, - MyDSpaceNewExternalDropdownComponent + MyDSpaceNewExternalDropdownComponent, + MyDspaceQaEventsNotificationsComponent, ]; @NgModule({ diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html new file mode 100644 index 0000000000..baf90fdf53 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html @@ -0,0 +1,28 @@ + + +
+ +
+
+ {{ + "mydspace.qa-event-notification.check.notification-info" + | translate : { num: source.totalEvents } + }} +
+ +
+
+
+
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss new file mode 100644 index 0000000000..ab33b46fca --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss @@ -0,0 +1,8 @@ + +.source-logo { + max-height: var(--ds-header-logo-height); +} + +.sections-gap { + gap: 1rem; +} diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts new file mode 100644 index 0000000000..4bf42c1319 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications.component'; +import { QualityAssuranceSourceDataService } from '../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service'; +import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; +import { createPaginatedList } from 'src/app/shared/testing/utils.test'; +import { QualityAssuranceSourceObject } from 'src/app/core/suggestion-notifications/qa/models/quality-assurance-source.model'; + +describe('MyDspaceQaEventsNotificationsComponent', () => { + let component: MyDspaceQaEventsNotificationsComponent; + let fixture: ComponentFixture; + + let qualityAssuranceSourceDataServiceStub: any; + const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()])); + + beforeEach(async () => { + qualityAssuranceSourceDataServiceStub = { + getSources: () => obj + }; + await TestBed.configureTestingModule({ + declarations: [ MyDspaceQaEventsNotificationsComponent ], + providers: [ + { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MyDspaceQaEventsNotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts new file mode 100644 index 0000000000..9992ec9ff8 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { QualityAssuranceSourceDataService } from '../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service'; +import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../core/shared/operators'; +import { Observable, of, tap } from 'rxjs'; +import { QualityAssuranceSourceObject } from 'src/app/core/suggestion-notifications/qa/models/quality-assurance-source.model'; + +@Component({ + selector: 'ds-my-dspace-qa-events-notifications', + templateUrl: './my-dspace-qa-events-notifications.component.html', + styleUrls: ['./my-dspace-qa-events-notifications.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MyDspaceQaEventsNotificationsComponent implements OnInit { + + /** + * An Observable that emits an array of QualityAssuranceSourceObject. + */ + sources$: Observable = of([]); + + constructor(private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService) { } + + ngOnInit(): void { + this.getSources(); + } + + /** + * Retrieves the sources for Quality Assurance. + * @returns An Observable of the sources for Quality Assurance. + * @throws An error if the retrieval of Quality Assurance sources fails. + */ + getSources() { + this.sources$ = this.qualityAssuranceSourceDataService.getSources() + .pipe( + getFirstCompletedRemoteData(), + tap((rd) => { + if (rd.hasFailed) { + throw new Error('Can\'t retrieve Quality Assurance sources'); + } + }), + getRemoteDataPayload(), + getPaginatedListPayload(), + ); + } +} diff --git a/src/app/shared/mocks/notifications.mock.ts b/src/app/shared/mocks/notifications.mock.ts index dc1c98c7b9..82ba818b13 100644 --- a/src/app/shared/mocks/notifications.mock.ts +++ b/src/app/shared/mocks/notifications.mock.ts @@ -1838,8 +1838,8 @@ export function getMockNotificationsStateService(): any { */ export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService { return jasmine.createSpyObj('QualityAssuranceTopicDataService', { - getTopics: jasmine.createSpy('getTopics'), - getTopic: jasmine.createSpy('getTopic'), + searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'), + searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'), }); } diff --git a/src/app/shared/mocks/suggestion.mock.ts b/src/app/shared/mocks/suggestion.mock.ts index fd1a7c41f1..8b1ab7acd5 100644 --- a/src/app/shared/mocks/suggestion.mock.ts +++ b/src/app/shared/mocks/suggestion.mock.ts @@ -1,7 +1,6 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Item } from '../../core/shared/item.model'; import { SearchResult } from '../search/models/search-result.model'; -import { SuggestionsService } from '../../suggestion-notifications/reciter-suggestions/suggestions.service'; // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 0f7871f7f9..6c9cf1c3b3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -284,6 +284,7 @@ import { } from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component'; import { NgxPaginationModule } from 'ngx-pagination'; +import { SplitPipe } from './utils/split.pipe'; const MODULES = [ CommonModule, @@ -323,7 +324,8 @@ const PIPES = [ ObjNgFor, BrowserOnlyPipe, MarkdownPipe, - ShortNumberPipe + ShortNumberPipe, + SplitPipe, ]; const COMPONENTS = [ diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts new file mode 100644 index 0000000000..4da1b1323e --- /dev/null +++ b/src/app/shared/utils/split.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'dsSplit' +}) +export class SplitPipe implements PipeTransform { + + transform(value: string, separator: string): string[] { + return value.split(separator); + } + +} diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 4a916cfe23..b3536a6fbf 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -9,7 +9,8 @@
- + [sectionData]="object"> +
+ diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html index e6ae9d1b9c..99bcec168f 100644 --- a/src/app/submission/sections/container/section-container.component.html +++ b/src/app/submission/sections/container/section-container.component.html @@ -48,4 +48,4 @@ -
\ No newline at end of file +
diff --git a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts new file mode 100644 index 0000000000..7b8d309667 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../../../core/data/base/data-service.decorator'; +import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; +import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; +import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data'; +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 { FindListOptions } from '../../../core/data/find-list-options.model'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { map, take } from 'rxjs/operators'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { MultipartPostRequest } from '../../../core/data/request.models'; +import { RestRequest } from '../../../core/data/rest-request.model'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; +import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; +import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data'; +import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data'; +import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; +import { Operation } from 'fast-json-patch'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; + + +/** + * A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint + */ +@Injectable() +@dataService(SUBMISSION_COAR_NOTIFY_CONFIG) +export class CoarNotifyConfigDataService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData { + createData: CreateDataImpl; + private findAllData: FindAllDataImpl; + private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; + private comparator: ChangeAnalyzer; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('submissioncoarnotifyconfigs', requestService, rdbService, objectCache, halService); + + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); + } + + + create(object: SubmissionCoarNotifyConfig): Observable> { + return this.createData.create(object); + } + + patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + update(object: SubmissionCoarNotifyConfig): Observable> { + return this.patchData.update(object); + } + + commitUpdates(method?: RestRequestMethod): void { + return this.patchData.commitUpdates(method); + } + + createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable { + return this.patchData.createPatchFromCache(object); + } + + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } + + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + public invoke(serviceName: string, serviceId: string, files: File[]): Observable> { + const requestId = this.requestService.generateRequestId(); + this.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'submissioncoarnotifyconfigmodel', serviceId).toString()), + map((endpoint: string) => { + const body = this.getInvocationFormData(files); + return new MultipartPostRequest(requestId, endpoint, body); + }) + ).subscribe((request: RestRequest) => this.requestService.send(request)); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + public SubmissionCoarNotifyConfigModelWithNameExistsAndCanExecute(scriptName: string): Observable { + return this.findById(scriptName).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + return hasValue(rd.payload); + }), + ); + } + + private getInvocationFormData(files: File[]): FormData { + const form: FormData = new FormData(); + files.forEach((file: File) => { + form.append('file', file); + }); + return form; + } +} diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts new file mode 100644 index 0000000000..53e41783ce --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts @@ -0,0 +1,13 @@ +/** + * The resource type for Ldn-Services + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +import { ResourceType } from '../../../core/shared/resource-type'; + + +export const SUBMISSION_COAR_NOTIFY_CONFIG = new ResourceType('submissioncoarnotifyconfig'); + +export const COAR_NOTIFY_WORKSPACEITEM = new ResourceType('workspaceitem'); + diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html new file mode 100644 index 0000000000..e3bdffd065 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html @@ -0,0 +1,124 @@ +
+ +
+ +
+
+
+
+ + +
+ +
+
+ + {{'submission.section.section-coar-notify.small.notification' | translate : {pattern : pattern} }} + + + + {{ error.message | translate}} + + +
+
+ +
+
{{ 'submission.section.section-coar-notify.selection.description' | translate }}
+
+ {{ ldnServiceByPattern[pattern][serviceIndex].description }} +
+ + + {{ 'submission.section.section-coar-notify.selection.no-description' | translate }} + + +
+
+
+
+
+
+ + {{ 'submission.section.section-coar-notify.notification.error' | translate }} + +
+
+
+
+
+
+ +

+ {{'submission.section.section-coar-notify.info.no-pattern' | translate }} +

+
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss new file mode 100644 index 0000000000..3ac8827f74 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss @@ -0,0 +1,4 @@ +// Getting styles for NgbDropdown +@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss'; +@import '../../../shared/form/form.component.scss'; + diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts new file mode 100644 index 0000000000..a66e24237f --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts @@ -0,0 +1,406 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify.component'; +import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionsService } from '../sections.service'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; +import { ChangeDetectorRef } from '@angular/core'; +import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { of } from 'rxjs'; +import { LdnService } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; +import { NotifyServicePattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model'; + +describe('SubmissionSectionCoarNotifyComponent', () => { + let component: SubmissionSectionCoarNotifyComponent; + let componentAsAny: any; + let fixture: ComponentFixture; + + let ldnServicesService: jasmine.SpyObj; + let coarNotifyConfigDataService: jasmine.SpyObj; + let operationsBuilder: jasmine.SpyObj; + let sectionService: jasmine.SpyObj; + let cdRefStub: any; + + const patterns: SubmissionCoarNotifyConfig[] = Object.assign( + [new SubmissionCoarNotifyConfig()], + { + patterns: ['review', 'endorsment'], + } + ); + const patternsPL = createPaginatedList(patterns); + const coarNotifyConfig = createSuccessfulRemoteDataObject$(patternsPL); + + beforeEach(async () => { + ldnServicesService = jasmine.createSpyObj('LdnServicesService', [ + 'findByInboundPattern', + ]); + coarNotifyConfigDataService = jasmine.createSpyObj( + 'CoarNotifyConfigDataService', + ['findAll'] + ); + operationsBuilder = jasmine.createSpyObj('JsonPatchOperationsBuilder', [ + 'remove', + 'replace', + 'add', + ]); + sectionService = jasmine.createSpyObj('SectionsService', [ + 'dispatchRemoveSectionErrors', + 'getSectionServerErrors', + 'setSectionError', + ]); + cdRefStub = Object.assign({ + detectChanges: () => fixture.detectChanges(), + }); + + await TestBed.configureTestingModule({ + declarations: [SubmissionSectionCoarNotifyComponent], + providers: [ + { provide: LdnServicesService, useValue: ldnServicesService }, + { provide: CoarNotifyConfigDataService, useValue: coarNotifyConfigDataService}, + { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, + { provide: SectionsService, useValue: sectionService }, + { provide: ChangeDetectorRef, useValue: cdRefStub }, + { provide: 'collectionIdProvider', useValue: 'collectionId' }, + { provide: 'sectionDataProvider', useValue: { id: 'sectionId', data: {} }}, + { provide: 'submissionIdProvider', useValue: 'submissionId' }, + NgbDropdown, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionSectionCoarNotifyComponent); + component = fixture.componentInstance; + componentAsAny = component; + + component.patterns = patterns[0].patterns; + coarNotifyConfigDataService.findAll.and.returnValue(coarNotifyConfig); + sectionService.getSectionServerErrors.and.returnValue( + of( + Object.assign([], { + path: 'sections/sectionId/data/notifyCoar', + message: 'error', + }) + ) + ); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('onSectionInit', () => { + it('should call setCoarNotifyConfig and getSectionServerErrorsAndSetErrorsToDisplay', () => { + spyOn(component, 'setCoarNotifyConfig'); + spyOn(componentAsAny, 'getSectionServerErrorsAndSetErrorsToDisplay'); + + component.onSectionInit(); + + expect(component.setCoarNotifyConfig).toHaveBeenCalled(); + expect(componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay).toHaveBeenCalled(); + }); + }); + + describe('onChange', () => { + const pattern = 'review'; + const index = 0; + const selectedService: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + { + pattern: 'review', + }, + ], + description: '', + }); + + beforeEach(() => { + component.ldnServiceByPattern[pattern] = []; + }); + + it('should do nothing if the selected value is the same as the previous one', () => { + component.ldnServiceByPattern[pattern][index] = selectedService; + component.onChange(pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.remove).not.toHaveBeenCalled(); + expect(componentAsAny.operationsBuilder.replace).not.toHaveBeenCalled(); + expect(componentAsAny.operationsBuilder.add).not.toHaveBeenCalled(); + }); + + it('should remove the path when the selected value is null', () => { + component.ldnServiceByPattern[pattern][index] = selectedService; + component.onChange(pattern, index, null); + + expect(componentAsAny.operationsBuilder.remove).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([pattern, index.toString()]) + ); + expect(component.ldnServiceByPattern[pattern][index]).toBeNull(); + expect(component.previousServices[pattern][index]).toBeNull(); + }); + + it('should replace the path when there is a previous value stored and it is different from the new one', () => { + const previousService: LdnService = Object.assign(new LdnService(), { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [ + { + pattern: 'endorsement', + }, + ], + description: 'test', + }); + component.ldnServiceByPattern[pattern][index] = previousService; + component.previousServices[pattern] = []; + component.previousServices[pattern][index] = previousService.id; + component.onChange(pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.replace).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([pattern, index.toString()]), + selectedService.id, + true + ); + expect(component.ldnServiceByPattern[pattern][index]).toEqual( + selectedService + ); + expect(component.previousServices[pattern][index]).toEqual( + selectedService.id + ); + }); + + it('should add the path when there is no previous value stored', () => { + component.onChange(pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([pattern, '-']), + [selectedService.id], + false, + true + ); + expect(component.ldnServiceByPattern[pattern][index]).toEqual( + selectedService + ); + expect(component.previousServices[pattern][index]).toEqual( + selectedService.id + ); + }); + }); + + describe('initSelectedServicesByPattern', () => { + const pattern1 = 'review'; + const pattern2 = 'endorsement'; + const service1: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern1, + }), + ], + }); + const service2: LdnService = Object.assign(new LdnService(), { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern2, + }), + ], + }); + const service3: LdnService = Object.assign(new LdnService(), { + id: 3, + name: 'service3', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern1, + }), + Object.assign(new NotifyServicePattern(), { + pattern: pattern2, + }), + ], + }); + + const services = [service1, service2, service3]; + + beforeEach(() => { + spyOn(component, 'filterServices').and.callFake((pattern) => { + return of( + services.filter((service) => + component.hasInboundPattern(service, pattern) + ) + ); + }); + }); + + it('should initialize the selected services by pattern', () => { + component.patterns = [pattern1, pattern2]; + component.initSelectedServicesByPattern(); + + expect(component.ldnServiceByPattern[pattern1]).toEqual([null]); + expect(component.ldnServiceByPattern[pattern2]).toEqual([null]); + }); + + it('should add the service to the selected services by pattern if the section data has a value for the pattern', () => { + component.patterns = [pattern1, pattern2]; + component.sectionData.data[pattern1] = [service1.id, service3.id]; + component.sectionData.data[pattern2] = [service2.id, service3.id]; + component.initSelectedServicesByPattern(); + + expect(component.ldnServiceByPattern[pattern1]).toEqual([ + service1, + service3, + ]); + expect(component.ldnServiceByPattern[pattern2]).toEqual([ + service2, + service3, + ]); + }); + }); + + describe('addService', () => { + const pattern = 'review'; + const service: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + + beforeEach(() => { + component.ldnServiceByPattern[pattern] = []; + }); + + it('should push the new service to the array corresponding to the pattern', () => { + component.addService(pattern, service); + + expect(component.ldnServiceByPattern[pattern]).toEqual([service]); + }); + }); + + describe('removeService', () => { + const pattern = 'review'; + const service1: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern, + }), + ], + }); + const service2: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern, + }), + ], + }); + const service3: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern, + }), + ], + }); + + beforeEach(() => { + component.ldnServiceByPattern[pattern] = [service1, service2, service3]; + }); + + it('should remove the service at the specified index from the array corresponding to the pattern', () => { + component.removeService(pattern, 1); + + expect(component.ldnServiceByPattern[pattern]).toEqual([ + service1, + service3, + ]); + }); + }); + + describe('filterServices', () => { + const pattern = 'review'; + const service1: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const service2: any = { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const service3: any = { + id: 3, + name: 'service3', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const services = [service1, service2, service3]; + + beforeEach(() => { + ldnServicesService.findByInboundPattern.and.returnValue( + createSuccessfulRemoteDataObject$(createPaginatedList(services)) + ); + }); + + it('should return an observable of the services that match the given pattern', () => { + component.filterServices(pattern).subscribe((result) => { + expect(result).toEqual(services); + }); + }); + }); + + describe('hasInboundPattern', () => { + const pattern = 'review'; + const service: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + + it('should return true if the service has the specified inbound pattern type', () => { + expect(component.hasInboundPattern(service, pattern)).toBeTrue(); + }); + + it('should return false if the service does not have the specified inbound pattern type', () => { + expect(component.hasInboundPattern(service, 'endorsement')).toBeFalse(); + }); + }); + + describe('getSectionServerErrorsAndSetErrorsToDisplay', () => { + it('should set the validation errors for the current section to display', () => { + const validationErrors = [ + { path: 'sections/sectionId/data/notifyCoar', message: 'error' }, + ]; + sectionService.getSectionServerErrors.and.returnValue( + of(validationErrors) + ); + + componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay(); + + expect(sectionService.setSectionError).toHaveBeenCalledWith( + component.submissionId, + component.sectionData.id, + validationErrors[0] + ); + }); + }); + + describe('onSectionDestroy', () => { + it('should unsubscribe from all subscriptions', () => { + const sub1 = of(null).subscribe(); + const sub2 = of(null).subscribe(); + componentAsAny.subs = [sub1, sub2]; + spyOn(sub1, 'unsubscribe'); + spyOn(sub2, 'unsubscribe'); + component.onSectionDestroy(); + expect(sub1.unsubscribe).toHaveBeenCalled(); + expect(sub2.unsubscribe).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts new file mode 100644 index 0000000000..92f56b4f83 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts @@ -0,0 +1,293 @@ +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionsService } from '../sections.service'; +import { SectionDataObject } from '../models/section-data.model'; + +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; + +import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; +import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { LdnService } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; +import { filter, map, take, tap } from 'rxjs/operators'; +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { SubmissionSectionError } from '../../objects/submission-section-error.model'; + +/** + * This component represents a section that contains the submission section-coar-notify form. + */ +@Component({ + selector: 'ds-submission-section-coar-notify', + templateUrl: './section-coar-notify.component.html', + styleUrls: ['./section-coar-notify.component.scss'], + providers: [NgbDropdown] +}) +@renderSectionFor(SectionsType.CoarNotify) +export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent { + + /** + * Contains an array of string patterns. + */ + patterns: string[] = []; + /** + * An object that maps string keys to arrays of LdnService objects. + * Used to store LdnService objects by pattern. + */ + ldnServiceByPattern: { [key: string]: LdnService[] } = {}; + /** + * A map representing all services for each pattern + * { + * 'pattern': { + * 'index': 'service.id' + * } + * } + * + * @type {{ [key: string]: {[key: number]: number} }} + * @memberof SubmissionSectionCoarNotifyComponent + */ + previousServices: { [key: string]: {[key: number]: number} } = {}; + + /** + * The [[JsonPatchOperationPathCombiner]] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + /** + * A map representing all field on their way to be removed + * @type {Map} + */ + protected fieldsOnTheirWayToBeRemoved: Map = new Map(); + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + constructor(protected ldnServicesService: LdnServicesService, + // protected formOperationsService: SectionFormOperationsService, + protected operationsBuilder: JsonPatchOperationsBuilder, + protected sectionService: SectionsService, + protected coarNotifyConfigDataService: CoarNotifyConfigDataService, + protected chd: ChangeDetectorRef, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + /** + * Initialize all instance variables + */ + onSectionInit() { + this.setCoarNotifyConfig(); + this.getSectionServerErrorsAndSetErrorsToDisplay(); + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + } + + /** + * Method called when section is initialized + * Retriev available NotifyConfigs + */ + setCoarNotifyConfig() { + this.subs.push( + this.coarNotifyConfigDataService.findAll().pipe( + getFirstCompletedRemoteData() + ).subscribe((data) => { + if (data.hasSucceeded) { + this.patterns = data.payload.page[0].patterns; + this.initSelectedServicesByPattern(); + } + })); + } + + /** + * Handles the change event of a select element. + * @param pattern - The pattern of the select element. + * @param index - The index of the select element. + */ + onChange(pattern: string, index: number, selectedService: LdnService | null) { + // do nothing if the selected value is the same as the previous one + if (this.ldnServiceByPattern[pattern][index]?.id === selectedService?.id) { + return; + } + + // initialize the previousServices object for the pattern if it does not exist + if (!this.previousServices[pattern]) { + this.previousServices[pattern] = {}; + } + + if (hasNoValue(selectedService)) { + // on value change, remove the path when the selected value is null + // and remove the previous value stored for the same index and pattern + this.operationsBuilder.remove(this.pathCombiner.getPath([pattern, index.toString()])); + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + this.ldnServiceByPattern[pattern][index] = null; + this.previousServices[pattern][index] = null; + return; + } + // store the previous value + this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index]?.id; + // set the new value + this.ldnServiceByPattern[pattern][index] = selectedService; + + const hasPrevValueStored = hasValue(this.previousServices[pattern][index]) && this.previousServices[pattern][index] !== selectedService.id; + if (hasPrevValueStored) { + // replace the path + // when there is a previous value stored and it is different from the new one + this.operationsBuilder.replace(this.pathCombiner.getPath([pattern, index.toString()]), selectedService.id, true); + } else { + // add the path when there is no previous value stored + this.operationsBuilder.add(this.pathCombiner.getPath([pattern, '-']), [selectedService.id], false, true); + } + // set the previous value to the new value + this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index].id; + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + this.chd.detectChanges(); + } + + /** + * Initializes the selected services by pattern. + * Loops through each pattern and filters the services based on the pattern. + * If the section data has a value for the pattern, it adds the service to the selected services by pattern. + * If the section data does not have a value for the pattern, it adds a null service to the selected services by pattern, + * so that the select element is initialized with a null value and to display the default select input. + */ + initSelectedServicesByPattern(): void { + this.patterns.forEach((pattern) => { + if (hasValue(this.sectionData.data[pattern])) { + this.subs.push( + this.filterServices(pattern) + .subscribe((services: LdnService[]) => { + const selectedServices = services.filter((service) => { + const selection = (this.sectionData.data[pattern] as LdnService[]).find((s: LdnService) => s.id === service.id); + this.addService(pattern, selection); + return this.sectionData.data[pattern].includes(service.id); + }); + this.ldnServiceByPattern[pattern] = selectedServices; + }) + ); + } else { + this.ldnServiceByPattern[pattern] = []; + this.addService(pattern, null); + } + }); + } + + /** + * Adds a new service to the selected services for the given pattern. + * @param pattern - The pattern to add the new service to. + * @param newService - The new service to add. + */ + addService(pattern: string, newService: LdnService) { + // Your logic to add a new service to the selected services for the pattern + // Example: Push the newService to the array corresponding to the pattern + if (!this.ldnServiceByPattern[pattern]) { + this.ldnServiceByPattern[pattern] = []; + } + this.ldnServiceByPattern[pattern].push(newService); + } + + /** + * Removes the service at the specified index from the array corresponding to the pattern. + * (part of next phase of implementation) + */ + removeService(pattern: string, serviceIndex: number) { + if (this.ldnServiceByPattern[pattern]) { + // Remove the service at the specified index from the array + this.ldnServiceByPattern[pattern].splice(serviceIndex, 1); + } + } + + /** + * Method called when dropdowns for the section are initialized + * Retrieve services with corresponding patterns to the dropdowns. + */ + filterServices(pattern: string): Observable { + return this.ldnServicesService.findByInboundPattern(pattern).pipe( + getFirstCompletedRemoteData(), + tap((rd) => { + if (rd.hasFailed) { + throw new Error(`Failed to retrieve services for pattern ${pattern}`); + } + }), + filter((rd) => rd.hasSucceeded), + getRemoteDataPayload(), + getPaginatedListPayload(), + map((res: LdnService[]) => res.filter((service) => + this.hasInboundPattern(service, pattern))) + ); + } + + /** + * Checks if the given service has the specified inbound pattern type. + * @param service - The service to check. + * @param patternType - The inbound pattern type to look for. + * @returns True if the service has the specified inbound pattern type, false otherwise. + */ + hasInboundPattern(service: any, patternType: string): boolean { + return service.notifyServiceInboundPatterns.some((pattern: { pattern: string }) => { + return pattern.pattern === patternType; + }); + } + + /** + * Retrieves server errors for the current section and sets them to display. + * @returns An Observable that emits the validation errors for the current section. + */ + private getSectionServerErrorsAndSetErrorsToDisplay() { + this.subs.push( + this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + ).subscribe((validationErrors: SubmissionSectionError[]) => { + if (isNotEmpty(validationErrors)) { + validationErrors.forEach((error) => { + this.sectionService.setSectionError(this.submissionId, this.sectionData.id, error); + }); + } + })); + } + + /** + * Returns an observable of the errors for the current section that match the given pattern and index. + * @param pattern - The pattern to match against the error paths. + * @param index - The index to match against the error paths. + * @returns An observable of the errors for the current section that match the given pattern and index. + */ + public getShownSectionErrors$(pattern: string, index: number): Observable { + return this.sectionService.getShownSectionErrors(this.submissionId, this.sectionData.id, this.sectionData.sectionType) + .pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + map((validationErrors: SubmissionSectionError[]) => { + return validationErrors.filter((error) => { + const path = `${pattern}/${index}`; + return error.path.includes(path); + }); + }) + ); + } + + /** + * @returns An observable that emits a boolean indicating whether the section has any server errors or not. + */ + protected getSectionStatus(): Observable { + return this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + map((validationErrors) => isEmpty(validationErrors) + )); + } + + /** + * Unsubscribe from all subscriptions + */ + onSectionDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts new file mode 100644 index 0000000000..41ef69cd7a --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts @@ -0,0 +1,35 @@ +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; + +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { COAR_NOTIFY_WORKSPACEITEM } from './section-coar-notify-service.resource-type'; + + +/** An CoarNotify and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject { + static type = COAR_NOTIFY_WORKSPACEITEM; + + @excludeFromEquals + @autoserialize + endorsement?: number[]; + + @deserializeAs('id') + review?: number[]; + + @autoserialize + ingest?: number[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts new file mode 100644 index 0000000000..04973f80c8 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts @@ -0,0 +1,39 @@ +import { ResourceType } from '../../../core/shared/resource-type'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; + +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; + + +/** A SubmissionCoarNotifyConfig and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyConfig extends CacheableObject { + static type = SUBMISSION_COAR_NOTIFY_CONFIG; + + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @deserializeAs('id') + uuid: string; + + @autoserialize + patterns: string[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 6bca8a7252..5f71d1731d 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -9,4 +9,5 @@ export enum SectionsType { SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', Collection = 'collection', + CoarNotify = 'coarnotify' } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index cf0ab2b369..d839565f8d 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -10,7 +10,7 @@ import { SubmissionFormFooterComponent } from './form/footer/submission-form-foo import { SubmissionFormComponent } from './form/submission-form.component'; import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; import { SubmissionSectionContainerComponent } from './sections/container/section-container.component'; -import { CommonModule } from '@angular/common'; +import { CommonModule, NgOptimizedImage } from '@angular/common'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { submissionReducers, SubmissionState } from './submission.reducers'; @@ -67,6 +67,11 @@ import { } from './sections/sherpa-policies/metadata-information/metadata-information.component'; import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component'; +import { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component'; +import { + CoarNotifyConfigDataService +} from './sections/section-coar-notify/coar-notify-config-data.service'; +import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -76,6 +81,7 @@ const ENTRY_COMPONENTS = [ SubmissionSectionCcLicensesComponent, SubmissionSectionAccessesComponent, SubmissionSectionSherpaPoliciesComponent, + SubmissionSectionCoarNotifyComponent ]; const DECLARATIONS = [ @@ -109,20 +115,22 @@ const DECLARATIONS = [ ]; @NgModule({ - imports: [ - CommonModule, - CoreModule.forRoot(), - SharedModule, - StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(submissionEffects), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - FormModule, - NgbModalModule, - NgbCollapseModule, - NgbAccordionModule, - UploadModule, - ], + imports: [ + CommonModule, + CoreModule.forRoot(), + SharedModule, + StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), + EffectsModule.forFeature(), + EffectsModule.forFeature(submissionEffects), + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents(), + FormModule, + NgbModalModule, + NgbCollapseModule, + NgbAccordionModule, + UploadModule, + NgOptimizedImage, + ], declarations: DECLARATIONS, exports: [ ...DECLARATIONS, @@ -135,6 +143,8 @@ const DECLARATIONS = [ SubmissionAccessesConfigDataService, SectionAccessesService, SectionFormOperationsService, + CoarNotifyConfigDataService, + LdnServicesService ] }) diff --git a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html index 7f1b166d24..5b4757ce8b 100644 --- a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html +++ b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html @@ -4,13 +4,19 @@

- + + + + + + {{(getTargetItemTitle() | async)}} +
@@ -54,6 +60,16 @@ target="_blank" [routerLink]="['/items', eventElement?.target?.id]">{{eventElement.title}} {{eventElement.title}} +
+ {{'quality-assurance.event.table.event.message.serviceName' | translate}} + {{eventElement.event.message.serviceName}} +
+
+ {{'quality-assurance.event.table.event.message.link' | translate}} + + {{eventElement.event.message.href}} + +

{{'quality-assurance.event.table.pidtype' | translate}} {{eventElement.event.message.type}}

@@ -157,7 +173,7 @@
- + {{'quality-assurance.events.back' | translate}} diff --git a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.spec.ts b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.spec.ts index 04ece87fbb..a4254962b2 100644 --- a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.spec.ts +++ b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.spec.ts @@ -42,6 +42,7 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; describe('QualityAssuranceEventsComponent test suite', () => { let fixture: ComponentFixture; @@ -118,6 +119,7 @@ describe('QualityAssuranceEventsComponent test suite', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: TranslateService, useValue: getMockTranslateService() }, { provide: PaginationService, useValue: paginationService }, + { provide: ItemDataService, useValue: {} }, QualityAssuranceEventsComponent ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.ts b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.ts index e34c121f35..fee2557a12 100644 --- a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.ts +++ b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.ts @@ -1,5 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; @@ -26,10 +27,12 @@ import { ProjectEntryImportModalComponent, QualityAssuranceEventData } from '../project-entry-import-modal/project-entry-import-modal.component'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { Item } from '../../../core/shared/item.model'; import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; +import { ItemDataService } from '../../../core/data/item-data.service'; /** * Component to display the Quality Assurance event list. @@ -105,6 +108,26 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { */ protected subs: Subscription[] = []; + /** + * The target item id, retrieved from the topic-id composition. + */ + public targetId: string; + + /** + * The URL of the item page/target. + */ + public itemPageUrl: string; + + /** + * Plain topic name (without the source id) + */ + public selectedTopicName: string; + + /** + * The source id, retrieved from the topic-id composition. + */ + public sourceId: string; + /** * Initialize the component variables. * @param {ActivatedRoute} activatedRoute @@ -120,7 +143,9 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private qualityAssuranceEventRestService: QualityAssuranceEventDataService, private paginationService: PaginationService, - private translateService: TranslateService + private translateService: TranslateService, + private itemService: ItemDataService, + private _location: Location ) { } @@ -137,10 +162,13 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { const regEx = /!/g; this.showTopic = id.replace(regEx, '/'); this.topic = id; + const splitList = this.showTopic?.split(':'); + this.targetId = splitList.length > 2 ? splitList.pop() : null; + this.sourceId = splitList[0]; + this.selectedTopicName = splitList[1]; return this.getQualityAssuranceEvents(); }) ).subscribe((events: QualityAssuranceEventData[]) => { - console.log(events); this.eventsUpdated$.next(events); this.isEventPageLoading.next(false); }); @@ -356,7 +384,6 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { if (rd.hasSucceeded) { this.totalElements$.next(rd.payload.totalElements); if (rd.payload.totalElements > 0) { - console.log(rd.payload.page); return this.fetchEvents(rd.payload.page); } else { return of([]); @@ -425,4 +452,37 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy { last() ); } + + /** + * Returns the page route for the given item. + * @param item The item to get the page route for. + * @returns The page route for the given item. + */ + public getItemPageRoute(item: Item): string { + return getItemPageRoute(item); + } + + /** + * Returns an Observable that emits the title of the target item. + * The target item is retrieved by its ID using the itemService. + * The title is extracted from the first metadata value of the item. + * The item page URL is also set in the component. + * @returns An Observable that emits the title of the target item. + */ + public getTargetItemTitle(): Observable { + return this.itemService.findById(this.targetId).pipe( + take(1), + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + tap((item: Item) => this.itemPageUrl = getItemPageRoute(item)), + map((item: Item) => item.firstMetadataValue('dc.title')) + ); + } + + /** + * Navigates back to the previous location in the browser's history stack. + */ + public goBack() { + this._location.back(); + } } diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.actions.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.actions.ts index 2459d4352a..482a85a988 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.actions.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.actions.ts @@ -25,6 +25,8 @@ export class RetrieveAllTopicsAction implements Action { payload: { elementsPerPage: number; currentPage: number; + source: string; + target?: string; }; /** @@ -35,10 +37,12 @@ export class RetrieveAllTopicsAction implements Action { * @param currentPage * The page number to retrieve */ - constructor(elementsPerPage: number, currentPage: number) { + constructor(elementsPerPage: number, currentPage: number, source: string, target?: string) { this.payload = { elementsPerPage, - currentPage + currentPage, + source, + target }; } } diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.html b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.html index db8586f264..5fa32d46fc 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.html +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.html @@ -2,7 +2,11 @@

{{'quality-assurance.title'| translate}}

- {{'quality-assurance.topics.description'| translate:{source: sourceId} }} + {{'quality-assurance.topics.description'| translate:{source: sourceId} }} + + {{'quality-assurance.topics.description-with-target'| translate:{source: sourceId} }} + {{(getTargetItemTitle() | async)}} +
@@ -15,7 +19,7 @@ [collectionSize]="(totalElements$ | async)" [hideGear]="false" [hideSortOptions]="true" - (paginationChange)="getQualityAssuranceTopics()"> + (paginationChange)="getQualityAssuranceTopics(sourceId, targetId)"> @@ -40,7 +44,7 @@ diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.spec.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.spec.ts index c80d2bce20..4137fdfae1 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.spec.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.spec.ts @@ -16,7 +16,7 @@ import { SuggestionNotificationsStateService } from '../../suggestion-notificati import { cold } from 'jasmine-marbles'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationService } from '../../../core/pagination/pagination.service'; -import { QualityAssuranceTopicsService } from './quality-assurance-topics.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; describe('QualityAssuranceTopicsComponent test suite', () => { let fixture: ComponentFixture; @@ -44,14 +44,14 @@ describe('QualityAssuranceTopicsComponent test suite', () => { providers: [ { provide: SuggestionNotificationsStateService, useValue: mockNotificationsStateService }, { provide: ActivatedRoute, useValue: { data: observableOf(activatedRouteParams), snapshot: { - paramMap: { - get: () => 'openaire', + params: { + sourceId: 'openaire', + targetId: null }, }}}, { provide: PaginationService, useValue: paginationService }, + { provide: ItemDataService, useValue: {} }, QualityAssuranceTopicsComponent, - // tslint:disable-next-line: no-empty - { provide: QualityAssuranceTopicsService, useValue: { setSourceId: (sourceId: string) => { } }} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents().then(() => { diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.ts index 3c8b4f8f38..0f7a111089 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; import { SortOptions } from '../../../core/cache/models/sort-options.model'; import { @@ -15,7 +15,10 @@ import { } from '../../../admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { ActivatedRoute } from '@angular/router'; -import { QualityAssuranceTopicsService } from './quality-assurance-topics.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { Item } from '../../../core/shared/item.model'; +import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; /** * Component to display the Quality Assurance topic list. @@ -60,6 +63,17 @@ export class QualityAssuranceTopicsComponent implements OnInit { */ public sourceId: string; + /** + * This property represents a targetId (item-id) which is used to retrive a topic + * @type {string} + */ + public targetId: string; + + /** + * The URL of the item page. + */ + public itemPageUrl: string; + /** * Initialize the component variables. * @param {PaginationService} paginationService @@ -71,16 +85,16 @@ export class QualityAssuranceTopicsComponent implements OnInit { private paginationService: PaginationService, private activatedRoute: ActivatedRoute, private notificationsStateService: SuggestionNotificationsStateService, - private qualityAssuranceTopicsService: QualityAssuranceTopicsService + private itemService: ItemDataService ) { + this.sourceId = this.activatedRoute.snapshot.params.sourceId; + this.targetId = this.activatedRoute.snapshot.params.targetId; } /** * Component initialization. */ ngOnInit(): void { - this.sourceId = this.activatedRoute.snapshot.paramMap.get('sourceId'); - this.qualityAssuranceTopicsService.setSourceId(this.sourceId); this.topics$ = this.notificationsStateService.getQualityAssuranceTopics(); this.totalElements$ = this.notificationsStateService.getQualityAssuranceTopicsTotals(); } @@ -93,7 +107,7 @@ export class QualityAssuranceTopicsComponent implements OnInit { this.notificationsStateService.isQualityAssuranceTopicsLoaded().pipe( take(1) ).subscribe(() => { - this.getQualityAssuranceTopics(); + this.getQualityAssuranceTopics(this.sourceId, this.targetId); }) ); } @@ -121,13 +135,15 @@ export class QualityAssuranceTopicsComponent implements OnInit { /** * Dispatch the Quality Assurance topics retrival. */ - public getQualityAssuranceTopics(): void { + public getQualityAssuranceTopics(source: string, target?: string): void { this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( distinctUntilChanged(), ).subscribe((options: PaginationComponentOptions) => { this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics( options.pageSize, - options.currentPage + options.currentPage, + source, + target ); }); } @@ -150,6 +166,32 @@ export class QualityAssuranceTopicsComponent implements OnInit { } } + /** + * Returns an Observable that emits the title of the target item. + * The target item is retrieved by its ID using the itemService. + * The title is extracted from the first metadata value of the item. + * The item page URL is also set in the component. + * @returns An Observable that emits the title of the target item. + */ + getTargetItemTitle(): Observable { + return this.itemService.findById(this.targetId).pipe( + take(1), + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + tap((item: Item) => this.itemPageUrl = getItemPageRoute(item)), + map((item: Item) => item.firstMetadataValue('dc.title')) + ); + } + + /** + * Returns the page route for the given item. + * @param item The item to get the page route for. + * @returns The page route for the given item. + */ + getItemPageRoute(item: Item): string { + return getItemPageRoute(item); + } + /** * Unsubscribe from all subscriptions. */ diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.effects.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.effects.ts index 13e3670000..92d7dc9e21 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.effects.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.effects.ts @@ -37,7 +37,9 @@ export class QualityAssuranceTopicsEffects { switchMap(([action, currentState]: [RetrieveAllTopicsAction, any]) => { return this.qualityAssuranceTopicService.getTopics( action.payload.elementsPerPage, - action.payload.currentPage + action.payload.currentPage, + action.payload.source, + action.payload.target ).pipe( map((topics: PaginatedList) => new AddTopicsAction(topics.page, topics.totalPages, topics.currentPage, topics.totalElements) diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.reducer.spec.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.reducer.spec.ts index a1c002d3f2..2872e41f94 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.reducer.spec.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.reducer.spec.ts @@ -29,7 +29,7 @@ describe('qualityAssuranceTopicsReducer test suite', () => { const expectedState = qualityAssuranceTopicInitialState; expectedState.processing = true; - const action = new RetrieveAllTopicsAction(elementPerPage, currentPage); + const action = new RetrieveAllTopicsAction(elementPerPage, currentPage, 'source', 'target'); const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action); expect(newState).toEqual(expectedState); diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.spec.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.spec.ts index 1e4e3fcffd..1faaa7f43d 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.spec.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.spec.ts @@ -42,31 +42,46 @@ describe('QualityAssuranceTopicsService', () => { beforeEach(() => { restService = TestBed.inject(QualityAssuranceTopicDataService); restServiceAsAny = restService; - restServiceAsAny.getTopics.and.returnValue(observableOf(paginatedListRD)); + restServiceAsAny.searchTopicsBySource.and.returnValue(observableOf(paginatedListRD)); + restServiceAsAny.searchTopicsByTarget.and.returnValue(observableOf(paginatedListRD)); service = new QualityAssuranceTopicsService(restService); serviceAsAny = service; }); - describe('getTopics', () => { - it('Should proxy the call to qualityAssuranceTopicRestService.getTopics', () => { + describe('getTopicsBySource', () => { + it('should proxy the call to qualityAssuranceTopicRestService.searchTopicsBySource', () => { const sortOptions = new SortOptions('name', SortDirection.ASC); const findListOptions: FindListOptions = { elementsPerPage: elementsPerPage, currentPage: currentPage, sort: sortOptions, - searchParams: [new RequestParam('source', 'ENRICH!MORE!ABSTRACT')] + searchParams: [new RequestParam('source', 'openaire')] }; - service.setSourceId('ENRICH!MORE!ABSTRACT'); - const result = service.getTopics(elementsPerPage, currentPage); - expect((service as any).qualityAssuranceTopicRestService.getTopics).toHaveBeenCalledWith(findListOptions); + const result = service.getTopics(elementsPerPage, currentPage, 'openaire'); + expect((service as any).qualityAssuranceTopicRestService.searchTopicsBySource).toHaveBeenCalledWith(findListOptions); }); - it('Should return a paginated list of Quality Assurance topics', () => { + it('should return a paginated list of Quality Assurance topics', () => { const expected = cold('(a|)', { a: paginatedList }); - const result = service.getTopics(elementsPerPage, currentPage); + const result = service.getTopics(elementsPerPage, currentPage, 'openaire'); expect(result).toBeObservable(expected); }); + + it('should include targetId in searchParams if set', () => { + const sortOptions = new SortOptions('name', SortDirection.ASC); + const findListOptions: FindListOptions = { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + sort: sortOptions, + searchParams: [ + new RequestParam('source', 'openaire'), + new RequestParam('target', '0000-0000-0000-0000-0000') + ] + }; + const result = service.getTopics(elementsPerPage, currentPage,'openaire', '0000-0000-0000-0000-0000'); + expect((service as any).qualityAssuranceTopicRestService.searchTopicsByTarget).toHaveBeenCalledWith(findListOptions); + }); }); }); diff --git a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.ts b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.ts index 6820791dff..4144cd8875 100644 --- a/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.ts +++ b/src/app/suggestion-notifications/qa/topics/quality-assurance-topics.service.ts @@ -13,6 +13,7 @@ import { import { RequestParam } from '../../../core/cache/models/request-param.model'; import { FindListOptions } from '../../../core/data/find-list-options.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; /** * The service handling all Quality Assurance topic requests to the REST service. @@ -28,10 +29,6 @@ export class QualityAssuranceTopicsService { private qualityAssuranceTopicRestService: QualityAssuranceTopicDataService ) { } - /** - * sourceId used to get topics - */ - sourceId: string; /** * Return the list of Quality Assurance topics managing pagination and errors. @@ -43,17 +40,25 @@ export class QualityAssuranceTopicsService { * @return Observable> * The list of Quality Assurance topics. */ - public getTopics(elementsPerPage, currentPage): Observable> { + public getTopics(elementsPerPage, currentPage, source: string, target?: string): Observable> { const sortOptions = new SortOptions('name', SortDirection.ASC); - const findListOptions: FindListOptions = { elementsPerPage: elementsPerPage, currentPage: currentPage, sort: sortOptions, - searchParams: [new RequestParam('source', this.sourceId)] + searchParams: [new RequestParam('source', source)] }; - return this.qualityAssuranceTopicRestService.getTopics(findListOptions).pipe( + let request$: Observable>>; + + if (hasValue(target)) { + findListOptions.searchParams.push(new RequestParam('target', target)); + request$ = this.qualityAssuranceTopicRestService.searchTopicsByTarget(findListOptions); + } else { + request$ = this.qualityAssuranceTopicRestService.searchTopicsBySource(findListOptions); + } + + return request$.pipe( getFirstCompletedRemoteData(), map((rd: RemoteData>) => { if (rd.hasSucceeded) { @@ -64,12 +69,4 @@ export class QualityAssuranceTopicsService { }) ); } - - /** - * set sourceId which is used to get topics - * @param sourceId string - */ - setSourceId(sourceId: string) { - this.sourceId = sourceId; - } } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts index 67678354ca..242698b234 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts @@ -17,7 +17,8 @@ describe('SuggestionsPopupComponent', () => { const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', { hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'), getCurrentUserSuggestionTargets: jasmine.createSpy('getCurrentUserSuggestionTargets'), - dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction') + dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction'), + dispatchRefreshUserSuggestionsAction: jasmine.createSpy('dispatchRefreshUserSuggestionsAction') }); const mockNotificationInterpolation = { count: 12, source: 'source', suggestionId: 'id', displayName: 'displayName' }; diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts index c0f94cadce..e6a98fbcd5 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts @@ -31,7 +31,6 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy { } public initializePopup() { - console.log('POPUP INIT dispatchRefreshUserSuggestionsAction'); this.reciterSuggestionStateService.dispatchRefreshUserSuggestionsAction(); const notifier = new Subject(); this.subscription = combineLatest([ diff --git a/src/app/suggestion-notifications/suggestion-notifications-state.service.spec.ts b/src/app/suggestion-notifications/suggestion-notifications-state.service.spec.ts index ac669ed954..16d0de974d 100644 --- a/src/app/suggestion-notifications/suggestion-notifications-state.service.spec.ts +++ b/src/app/suggestion-notifications/suggestion-notifications-state.service.spec.ts @@ -271,8 +271,8 @@ describe('NotificationsStateService', () => { it('Should call store.dispatch', () => { const elementsPerPage = 3; const currentPage = 1; - const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage); - service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage); + const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage, 'source', 'target'); + service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage, 'source', 'target'); expect(serviceAsAny.store.dispatch).toHaveBeenCalledWith(action); }); }); diff --git a/src/app/suggestion-notifications/suggestion-notifications-state.service.ts b/src/app/suggestion-notifications/suggestion-notifications-state.service.ts index ec1ea2e039..1fc5422018 100644 --- a/src/app/suggestion-notifications/suggestion-notifications-state.service.ts +++ b/src/app/suggestion-notifications/suggestion-notifications-state.service.ts @@ -118,8 +118,8 @@ export class SuggestionNotificationsStateService { * @param currentPage * The number of the current page. */ - public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number): void { - this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage)); + public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number, sourceId: string, targteId?: string): void { + this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage, sourceId, targteId)); } // Quality Assurance source diff --git a/src/app/suggestions-page/suggestions-page-routing.module.ts b/src/app/suggestions-page/suggestions-page-routing.module.ts index 20ed658707..05dc6321b7 100644 --- a/src/app/suggestions-page/suggestions-page-routing.module.ts +++ b/src/app/suggestions-page/suggestions-page-routing.module.ts @@ -5,7 +5,6 @@ import { SuggestionsPageResolver } from './suggestions-page.resolver'; import { SuggestionsPageComponent } from './suggestions-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; @NgModule({ imports: [ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 40f0150022..3cf74b2b24 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -892,11 +892,11 @@ "coar-notify-support.title": "COAR Notify Protocol", - "coar-notify-support-title.content":"Here, we fully support the COAR Notify protocol, which is designed to enhance the communication between repositories. To learn more about the COAR Notify protocol, you can visit their official website here.", + "coar-notify-support-title.content": "Here, we fully support the COAR Notify protocol, which is designed to enhance the communication between repositories. To learn more about the COAR Notify protocol, you can visit their official website here.", "coar-notify-support.ldn-inbox.title": "LDN InBox", - "coar-notify-support.ldn-inbox.content": "For your convenience, our LDN (Linked Data Notifications) InBox is easily accessible at {restApiUrl}ldn/inbox. The LDN InBox enables seamless communication and data exchange, ensuring efficient and effective collaboration.", + "coar-notify-support.ldn-inbox.content": "For your convenience, our LDN (Linked Data Notifications) InBox is easily accessible at {ldnInboxUrl}ldn/inbox. The LDN InBox enables seamless communication and data exchange, ensuring efficient and effective collaboration.", "coar-notify-support.message-moderation.title": "Message Moderation", @@ -905,7 +905,7 @@ "service.overview.delete.header": "Delete Service", "ldn-registered-services.title": "Registered Services", - "ldn-registered-services.table.name":"Name", + "ldn-registered-services.table.name": "Name", "ldn-registered-services.table.description": "Description", "ldn-registered-services.table.status": "Status", "ldn-registered-services.table.action": "Action", @@ -920,7 +920,7 @@ "ldn-edit-registered-service.title": "Edit Service", "ldn-create-service.title": "Create service", "service.overview.create.modal": "Create Service", - "service.overview.create.body": "Please confirm the creation of this service", + "service.overview.create.body": "Please confirm the creation of this service.", "ldn-service-status": "Status", "service.confirm.create": "Create", "service.refuse.create": "Discard", @@ -929,16 +929,77 @@ "ldn-new-service.form.label.name": "Name", "ldn-new-service.form.label.description": "Description", "ldn-new-service.form.label.url": "Service URL", + "ldn-new-service.form.label.score": "Level of trust", "ldn-new-service.form.label.ldnUrl": "LDN Inbox URL", "ldn-new-service.form.placeholder.name": "Please provide service name", "ldn-new-service.form.placeholder.description": "Please provide a description regarding your service", "ldn-new-service.form.placeholder.url": "Please input the URL for users to check out more information about the service", "ldn-new-service.form.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox", - "ldn-new-service.form.label.inboundPattern": "Inbound Patterns", - "ldn-new-service.form.label.placeholder.inboundPattern": "Select an Inbound Pattern", + "ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator", + "ldn-service.form.label.placeholder.default-select": "Select a pattern", + + "ldn-service.form.pattern.ack-accept.label": "Acknowledge and Accept", + "ldn-service.form.pattern.ack-accept.description": "This pattern is used to acknowledge and accept a request (offer). It implies an intention to act on the request.", + "ldn-service.form.pattern.ack-accept.category": "Acknowledgements", + + "ldn-service.form.pattern.ack-reject.label": "Acknowledge and Reject", + "ldn-service.form.pattern.ack-reject.description": "This pattern is used to acknowledge and reject a request (offer). It signifies no further action regarding the request.", + "ldn-service.form.pattern.ack-reject.category": "Acknowledgements", + + "ldn-service.form.pattern.ack-tentative-accept.label": "Acknowledge and Tentatively Accept", + "ldn-service.form.pattern.ack-tentative-accept.description": "This pattern is used to acknowledge and tentatively accept a request (offer). It implies an intention to act, which may change.", + "ldn-service.form.pattern.ack-tentative-accept.category": "Acknowledgements", + + "ldn-service.form.pattern.ack-tentative-reject.label": "Acknowledge and Tentatively Reject", + "ldn-service.form.pattern.ack-tentative-reject.description": "This pattern is used to acknowledge and tentatively reject a request (offer). It signifies no further action, subject to change.", + "ldn-service.form.pattern.ack-tentative-reject.category": "Acknowledgements", + + "ldn-service.form.pattern.announce-endorsement.label": "Announce Endorsement", + "ldn-service.form.pattern.announce-endorsement.description": "This pattern is used to announce the existence of an endorsement, referencing the endorsed resource.", + "ldn-service.form.pattern.announce-endorsement.category": "Announcements", + + "ldn-service.form.pattern.announce-ingest.label": "Announce Ingest", + "ldn-service.form.pattern.announce-ingest.description": "This pattern is used to announce that a resource has been ingested.", + "ldn-service.form.pattern.announce-ingest.category": "Announcements", + + "ldn-service.form.pattern.announce-relationship.label": "Announce Relationship", + "ldn-service.form.pattern.announce-relationship.description": "This pattern is used to announce a relationship between two resources.", + "ldn-service.form.pattern.announce-relationship.category": "Announcements", + + "ldn-service.form.pattern.announce-review.label": "Announce Review", + "ldn-service.form.pattern.announce-review.description": "This pattern is used to announce the existence of a review, referencing the reviewed resource.", + "ldn-service.form.pattern.announce-review.category": "Announcements", + + "ldn-service.form.pattern.announce-service-result.label": "Announce Service Result", + "ldn-service.form.pattern.announce-service-result.description": "This pattern is used to announce the existence of a 'service result', referencing the relevant resource.", + "ldn-service.form.pattern.announce-service-result.category": "Announcements", + + "ldn-service.form.pattern.request-endorsement.label": "Request Endorsement", + "ldn-service.form.pattern.request-endorsement.description": "This pattern is used to request endorsement of a resource owned by the origin system.", + "ldn-service.form.pattern.request-endorsement.category": "Requests", + + "ldn-service.form.pattern.request-ingest.label": "Request Ingest", + "ldn-service.form.pattern.request-ingest.description": "This pattern is used to request that the target system ingest a resource.", + "ldn-service.form.pattern.request-ingest.category": "Requests", + + "ldn-service.form.pattern.request-review.label": "Request Review", + "ldn-service.form.pattern.request-review.description": "This pattern is used to request a review of a resource owned by the origin system.", + "ldn-service.form.pattern.request-review.category": "Requests", + + "ldn-service.form.pattern.undo-offer.label": "Undo Offer", + "ldn-service.form.pattern.undo-offer.description": "This pattern is used to undo (retract) an offer previously made.", + "ldn-service.form.pattern.undo-offer.category": "Undo", + "ldn-new-service.form.label.placeholder.selectedItemFilter": "No Item Filter Selected", "ldn-new-service.form.label.ItemFilter": "Item Filter", "ldn-new-service.form.label.automatic": "Automatic", + "ldn-new-service.form.error.name": "Name is required", + "ldn-new-service.form.error.url": "URL is required", + "ldn-new-service.form.error.ldnurl": "LDN URL is required", + "ldn-new-service.form.error.patterns": "At least a pattern is required", + "ldn-new-service.form.error.score": "Please enter a valid score (between 0 and 1). Use the “.” as decimal separator", + + "ldn-new-service.form.label.inboundPattern": "Inbound Pattern", "ldn-new-service.form.label.outboundPattern": "Outbound Patterns", "ldn-new-service.form.label.placeholder.outboundPattern": "Select an Outbound Pattern", "ldn-new-service.form.label.addPattern": "+ Add more", @@ -951,11 +1012,15 @@ "service.detail.return": "Cancel", "service.overview.reset-form.body": "Are you sure you want to discard those changes and leave?", "service.overview.reset-form.modal": "Discard Service Changes", - "service.overview.reset-form.reset-confirm":"Discard", + "service.overview.reset-form.reset-confirm": "Discard", "admin.registries.services-formats.modify.success.head": "Successful Edit", "admin.registries.services-formats.modify.success.content": "The service has been edited", + "admin.registries.services-formats.modify.failure.head": "Failed Edit", + "admin.registries.services-formats.modify.failure.content": "The service has not been edited", "ldn-service-notification.created.success.title": "Successful Create", "ldn-service-notification.created.success.body": "The service has been created", + "ldn-service-notification.created.failure.title": "Failed Create", + "ldn-service-notification.created.failure.body": "The service has not been created", "ldn-enable-service.notification.success.title": "Successful status updated", "ldn-enable-service.notification.success.content": "The service status has been updated", "ldn-service-delete.notification.success.title": "Successful Deletion", @@ -1978,10 +2043,12 @@ "info.feedback.breadcrumbs": "Feedback", - "info.coar-notify-support.title":"Notify Support", + "info.coar-notify-support.title": "Notify Support", "info.coar-notify.breadcrumbs": "Notify Support", + "submission.sections.notify.info": "The selected service is compatible with the item according to its current status. {{ service.name }}: {{ service.description }}", + "info.feedback.head": "Feedback", "info.feedback.title": "Feedback", @@ -2472,6 +2539,14 @@ "item.truncatable-part.show-less": "Collapse", + "item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + + "item.qa-event-notification-info.check.button": "Check", + + "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + + "mydspace.qa-event-notification-info.check.button": "Check", + "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", "workflow-item.search.result.delete-supervision.modal.info": "Are you sure you want to delete Supervision Order", @@ -2494,6 +2569,12 @@ "item.page.citation": "Citation", + "item.page.endorsed-by": "Endorsement", + + "item.page.is-reviewed-by": "Review", + + "item.page.is-supplemented-by": "Dataset", + "item.page.collections": "Collections", "item.page.collections.loading": "Loading...", @@ -3210,6 +3291,8 @@ "quality-assurance.topics.description": "Below you can see all the topics received from the subscriptions to {{source}}.", + "quality-assurance.topics.description-with-target": "Below you can see all the topics received from the subscriptions to {{source}} in regards to the", + "quality-assurance.source.description": "Below you can see all the notification's sources.", "quality-assurance.topics": "Current Topics", @@ -3236,7 +3319,9 @@ "quality-assurance.source.error.service.retrieve": "An error occurred while loading the Quality Assurance source", - "quality-assurance.events.description": "Below the list of all the suggestions for the selected topic.", + "quality-assurance.events.description": "Below the list of all the suggestions for the selected topic {{topic}}, related to {{source}}.", + + "quality-assurance.events.description-with-topic-and-target": "Below the list of all the suggestions for the selected topic {{topic}}, related to {{source}} and ", "quality-assurance.loading": "Loading ...", @@ -3290,6 +3375,10 @@ "quality-assurance.event.table.more": "Show more", + "quality-assurance.event.table.event.message.serviceName": "Service Name:", + + "quality-assurance.event.table.event.message.link": "Link:", + "quality-assurance.event.project.found": "Bound to the local record:", "quality-assurance.event.project.notFound": "No local record found", @@ -4760,6 +4849,8 @@ "submission.sections.submit.progressbar.sherpaPolicies": "Publisher open access policy information", + "submission.sections.submit.progressbar.coarnotify": "COAR Notify", + "submission.sections.sherpa-policy.title-empty": "No publisher policy information available. If your work has an associated ISSN, please enter it above to see any related publisher open access policies.", "submission.sections.status.errors.title": "Errors", @@ -5006,6 +5097,28 @@ "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", + "submission.section.section-coar-notify.control.request-review.label": "You can request a review to one of the following services", + + "submission.section.section-coar-notify.control.request-endorsement.label": "You can request an Endorsement to one of the following overlay journals", + + "submission.section.section-coar-notify.control.request-ingest.label": "You can request to ingest a copy of your submission to one of the following services", + + "submission.section.section-coar-notify.dropdown.no-data": "No data available", + + "submission.section.section-coar-notify.dropdown.select-none": "Select none", + + "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + + "submission.section.section-coar-notify.selection.description": "Selected service's description:", + + "submission.section.section-coar-notify.selection.no-description": "No further information is available", + + "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item. Please check the description for details about which record can be managed by this service.", + + "submission.section.section-coar-notify.info.no-pattern": "No patterns found in the submission.", + + "error.validation.coarnotify.invalidfilter": "Invalid filter, try to select another service or none.", + "submitter.empty": "N/A", "subscriptions.title": "Subscriptions", diff --git a/src/assets/i18n/it.json5 b/src/assets/i18n/it.json5 index 4131d0bee6..2c4c39987a 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -3723,6 +3723,22 @@ // "item.truncatable-part.show-less": "Collapse", "item.truncatable-part.show-less": "Riduci", + // "item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + // TODO New key - Add a translation + "item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + + // "item.qa-event-notification-info.check.button": "Check", + // TODO New key - Add a translation + "item.qa-event-notification-info.check.button": "Check", + + // "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + // TODO New key - Add a translation + "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", + + // "mydspace.qa-event-notification-info.check.button": "Check", + // TODO New key - Add a translation + "mydspace.qa-event-notification-info.check.button": "Check", + // "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", // TODO New key - Add a translation "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", @@ -7461,6 +7477,25 @@ // "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", "submission.workspace.generic.view-help": "Seleziona questa opzione per vedere i metadata dell'item.", + // "submission.section.section-coar-notify.dropdown.no-data": "No data available", + // TODO New key - a translation + "submission.section.section-coar-notify.dropdown.no-data": "No data available", + + // "submission.section.section-coar-notify.dropdown.select-none": "Select none", + // TODO New key - a translation + "submission.section.section-coar-notify.dropdown.select-none": "Select none", + + // "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + // TODO New key - a translation + "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + + // "submission.section.section-coar-notify.selection.description": "Selected service's description:", + // TODO New key - a translation + "submission.section.section-coar-notify.selection.description": "Selected service's description:", + + // "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.", + // TODO New key - a translation + "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.", // "subscriptions.title": "Subscriptions", "subscriptions.title": "Sottoscrizioni", diff --git a/src/assets/images/qa-coar-notify-logo.png b/src/assets/images/qa-coar-notify-logo.png new file mode 100644 index 0000000000..0ba021dfd2 Binary files /dev/null and b/src/assets/images/qa-coar-notify-logo.png differ diff --git a/src/assets/images/qa-openaire-logo.png b/src/assets/images/qa-openaire-logo.png new file mode 100644 index 0000000000..359fd73b84 Binary files /dev/null and b/src/assets/images/qa-openaire-logo.png differ diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index cb9d2c7130..806a45fbd5 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -262,6 +262,10 @@ export const environment: BuildConfig = { undoTimeout: 10000 // 10 seconds } }, + + + suggestion: [], + themes: [ { name: 'full-item-page-theme',