CST-12180

This commit is contained in:
Mattia Vianelli
2023-11-20 16:21:25 +01:00
102 changed files with 4874 additions and 1980 deletions

View File

@@ -1,34 +1,34 @@
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { RouterModule } from '@angular/router'; import {RouterModule} from '@angular/router';
import { I18nBreadcrumbResolver } from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver'; import {I18nBreadcrumbResolver} from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component';
import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component';
import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.component'; import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: LdnServicesOverviewComponent, component: LdnServicesOverviewComponent,
resolve: {breadcrumb: I18nBreadcrumbResolver}, resolve: {breadcrumb: I18nBreadcrumbResolver},
data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'}, data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'},
}, },
{ {
path: 'new', path: 'new',
resolve: {breadcrumb: I18nBreadcrumbResolver}, resolve: {breadcrumb: I18nBreadcrumbResolver},
component: LdnServiceNewComponent, component: LdnServiceNewComponent,
data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'} data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'}
}, },
{ {
path: 'edit/:serviceId', path: 'edit/:serviceId',
resolve: {breadcrumb: I18nBreadcrumbResolver}, resolve: {breadcrumb: I18nBreadcrumbResolver},
component: LdnServiceFormEditComponent, component: LdnServiceFormEditComponent,
data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'} data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'}
}, },
]), ]),
] ]
}) })
export class AdminLdnServicesRoutingModule { export class AdminLdnServicesRoutingModule {

View File

@@ -1,29 +1,29 @@
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { CommonModule } from '@angular/common'; import {CommonModule} from '@angular/common';
import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module'; import {AdminLdnServicesRoutingModule} from './admin-ldn-services-routing.module';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component';
import { SharedModule } from '../../shared/shared.module'; import {SharedModule} from '../../shared/shared.module';
import { LdnServiceNewComponent } from './ldn-service-new/ldn-service-new.component'; import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; import {LdnServiceFormComponent} from './ldn-service-form/ldn-service-form.component';
import { LdnServiceFormEditComponent } from './ldn-service-form-edit/ldn-service-form-edit.component'; import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component';
import { FormsModule } from '@angular/forms'; import {FormsModule} from '@angular/forms';
import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service'; import {LdnItemfiltersService} from './ldn-services-data/ldn-itemfilters-data.service';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
AdminLdnServicesRoutingModule, AdminLdnServicesRoutingModule,
FormsModule FormsModule
], ],
declarations: [ declarations: [
LdnServicesOverviewComponent, LdnServicesOverviewComponent,
LdnServiceNewComponent, LdnServiceNewComponent,
LdnServiceFormComponent, LdnServiceFormComponent,
LdnServiceFormEditComponent, LdnServiceFormEditComponent,
], ],
providers: [LdnItemfiltersService] providers: [LdnItemfiltersService]
}) })
export class AdminLdnServicesModule { export class AdminLdnServicesModule {
} }

View File

@@ -1,281 +1,412 @@
<div class="container"> <div class="container">
<form (ngSubmit)="onSubmit()" [formGroup]="formModel"> <form (ngSubmit)="onSubmit()" [formGroup]="formModel">
<div class="d-flex"> <div class="d-flex">
<h2 class="flex-grow-1">{{ 'ldn-edit-registered-service.title' | translate }}</h2> <h2 class="flex-grow-1">{{ 'ldn-edit-registered-service.title' | translate }}</h2>
</div>
<!-- In the toggle section -->
<div class="toggle-switch-container">
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
<div>
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
<div class="slider"></div>
</div> </div>
<!-- In the toggle section --> </div>
<div class="toggle-switch-container"> </div>
<label class="status-label" for="enabled">{{ 'ldn-service-status' | translate }}</label> <!-- In the Name section -->
<div> <div class="mb-5">
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox"> <label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch"> <input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
<div class="slider"></div> [placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
formControlName="name"
id="name"
name="name"
type="text">
<div *ngIf="formModel.get('name').invalid && formModel.get('name').touched" class="error-text">
{{ 'ldn-new-service.form.error.name' | translate }}
</div>
</div>
<!-- In the description section -->
<div class="mb-5 mt-5 d-flex flex-column">
<label for="description" class="font-weight-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
class="form-control" formControlName="description" id="description" name="description"></textarea>
</div>
<!-- In the url section -->
<div class="mb-5 mt-5">
<label for="url" class="font-weight-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
formControlName="url"
id="url"
name="url"
type="text">
<div *ngIf="formModel.get('url').invalid && formModel.get('url').touched" class="error-text">
{{ 'ldn-new-service.form.error.url' | translate }}
</div>
</div>
<!-- In the ldnUrl section -->
<div class="mb-5 mt-5">
<label for="ldnUrl" class="font-weight-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
formControlName="ldnUrl"
id="ldnUrl"
name="ldnUrl"
type="text">
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" class="error-text">
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
</div>
</div>
<!-- In the score section -->
<div class="mb-2">
<label for="score" class="font-weight-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
id="score"
name="score"
class="form-control"
type="text">
<div *ngIf="formModel.get('score').invalid && formModel.get('score').touched" class="error-text">
{{ 'ldn-new-service.form.error.score' | translate }}
</div>
</div>
<!-- In the Inbound Patterns Labels section -->
<div class="row mb-1 mt-5">
<div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div>
<ng-container *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern)">
<div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
<div class="col-sm-1">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
</div>
</ng-container>
<div class="col-sm-2">
</div>
</div>
<!-- In the Inbound Patterns section -->
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<div class="row mb-1">
<div class="col">
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
id="additionalInboundPattern{{i}}"
ngbDropdown placement="bottom-start">
<div class="position-relative right-addon" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="inboundPatternDropdown.open();"
[readonly]="true"
class="form-control w-80 scrollable-dropdown-input"
formControlName="patternLabel"
id="inboundPatternDropdownButton"
ngbDropdownAnchor
type="text"
/>
<div aria-labelledby="inboundPatternDropdownButton"
class="dropdown-menu scrollable-dropdown-menu w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem
type="button">
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
<div
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
</button>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
<div class="mb-2"> <div class="col">
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label> <ng-container
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched" *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern)">
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" formControlName="name" id="name" <div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
name="name" placement="bottom-start">
type="text"> <div class="position-relative right-addon" role="combobox">
</div> <i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<div class="mb-4"> <input
&nbsp; (click)="inboundItemfilterDropdown.open();"
</div> [readonly]="true"
[value]="selectedInboundItemfilters"
<!-- In the description section --> class="form-control w-100 scrollable-dropdown-input"
<div class="mb-2 d-flex flex-column"> formControlName="constraint"
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label> id="inboundItemfilterDropdown"
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate" ngbDropdownAnchor
formControlName="description" id="description" name="description"></textarea> type="text"
</div> />
<div aria-labelledby="inboundItemfilterDropdownButton"
<div class="mb-4"> class="dropdown-menu scrollable-dropdown-menu w-100 "
&nbsp; ngbDropdownMenu>
</div> <div class="scrollable-menu" role="listbox">
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
<!-- In the url section --> *ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
<div class="mb-2"> class="dropdown-item collection-item text-truncate w-100"
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label> ngbDropdownItem
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched" type="button">
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" formControlName="url" id="url" <div>{{ constraint.id }}</div>
name="url" </button>
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the ldnUrl section -->
<div class="mb-2">
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" formControlName="ldnUrl"
id="ldnUrl"
name="ldnUrl"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<div class="row">
<div class="col">
<label>{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div>
<div class="col">
<label class="label-box">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
<div class="col-sm1 ">
<label class="label-box-2">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
</div>
<div class="col-sm-1">
</div>
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<div [class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)" class="row mb-1">
<div class="col">
<select #inboundPattern formControlName="pattern" id="additionalInboundPattern{{i}}"
name="additionalInboundPattern{{i}}" required>
<option value="">{{ 'ldn-new-service.form.label.placeholder.inboundPattern' | translate }}</option>
<option *ngFor="let pattern of inboundPatterns"
[ngValue]="pattern.name">{{ pattern.name }}</option>
</select>
</div>
<div class="col">
<ng-container *ngIf="inboundPattern.value">
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}">
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option>
<option *ngFor="let itemFilter of (itemfiltersRD$ | async)?.payload?.page"
[value]="itemFilter.id">{{ itemFilter.id }}</option>
</select>
</ng-container>
</div>
<div [style.visibility]="inboundPattern.value ? 'visible' : 'hidden'" class="col-sm1">
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
type="checkbox">
<div (click)="toggleAutomatic(i)"
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
class="toggle-switch">
<div class="slider"></div>
</div>
</div>
<div class="col-sm-1 btn-group">
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button" type="button">
<i class="fas fa-trash"></i>
</button>
<button (click)="unmarkForInboundPatternDeletion(i)" *ngIf="markedForDeletionInboundPattern.includes(i)" class="btn btn-outline-dark undo-button"
type="button">
<i class="fas fa-undo"></i>
</button>
</div> </div>
</div>
</div> </div>
</div>
</ng-container> </ng-container>
</div>
<div
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
class="col-sm-1">
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
type="checkbox">
<div (click)="toggleAutomatic(i)"
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
class="toggle-switch">
<div class="slider"></div>
</div>
</div>
<div class="col-sm-2">
<div class="btn-group">
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
type="button">
<i class="fas fa-trash"></i>
</button>
<button (click)="unmarkForInboundPatternDeletion(i)"
*ngIf="markedForDeletionInboundPattern.includes(i)"
class="btn btn-warning "
type="button">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div> </div>
</ng-container>
</div>
<span (click)="addInboundPattern()" <span (click)="addInboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span> class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
<div class="mb-4"> <!-- In the Outbound Patterns Labels section -->
&nbsp; <div class="row mb-1 mt-5">
<div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.outboundPattern' | translate }} </label>
</div>
<ng-container *ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][0]?.value?.pattern)">
<div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div> </div>
</ng-container>
<div class="col-sm-1 ">
<label class="label-box-2" style="visibility: hidden;">
{{ 'ldn-new-service.form.label.automatic' | translate }}
</label>
</div>
<div class="col-sm-2 ">
</div>
</div>
<div class="row"> <!-- In the Outbound Patterns section -->
<div class="col"> <div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
<label>{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label> [class.marked-for-deletion]="markedForDeletionOutboundPattern.includes(i)"
</div> formGroupName="notifyServiceOutboundPatterns">
<div class="col">
<label class="label-box-3">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
<div class="col-sm-1 ">
<label class="label-box-2"></label>
</div>
<div class="col-sm-1 ">
</div>
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index" <ng-container [formGroupName]="i">
formGroupName="notifyServiceOutboundPatterns">
<ng-container [formGroupName]="i"> <div class="row mb-1">
<div class="col">
<!-- Input elements in a separate row --> <div #outboundPatternDropdown="ngbDropdown" class="w-100" id="additionalOutboundPattern{{i}}"
<div [class.marked-for-deletion]="markedForDeletionOutboundPattern.includes(i)" class="row mb-1"> ngbDropdown
<div class="col"> placement="bottom-start">
<select #outboundPattern formControlName="pattern" id="additionalOutboundPattern{{i}}" <div class="position-relative right-addon" role="combobox">
name="additionalOutboundPattern{{i}}" <i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
required> ngbDropdownToggle></i>
<option value="">{{ 'ldn-new-service.form.label.placeholder.outboundPattern' | translate }}</option> <input
<option *ngFor="let pattern of outboundPatterns" (click)="outboundPatternDropdown.open();"
[ngValue]="pattern.name">{{ pattern.name }}</option> [readonly]="true"
</select> [value]="selectedOutboundPatterns"
</div> class="form-control w-100 scrollable-dropdown-input"
<div class="col"> formControlName="patternLabel"
<ng-container *ngIf="outboundPattern.value"> id="outboundPatternDropdownButton"
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}"> ngbDropdownAnchor
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option> type="text"
<option *ngFor="let itemFilter of (itemfiltersRD$ | async)?.payload?.page" />
[value]="itemFilter.id">{{ itemFilter.id }}</option> <div aria-labelledby="outboundPatternDropdownButton"
</select> class="dropdown-menu scrollable-dropdown-menu w-100 "
</ng-container> ngbDropdownMenu>
</div> <div class="scrollable-menu" role="listbox">
<button (click)="selectOutboundPattern(pattern, i); $event.stopPropagation()"
<div [style.visibility]="'hidden'" class="col-sm1"> *ngFor="let pattern of outboundPatterns; let internalIndex = index"
<input hidden id="automatic{{i}}" name="automatic{{i}}" type="checkbox"> [title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
<div class="dropdown-item collection-item text-truncate w-100"
class="toggle-switch"> ngbDropdownItem
<div class="slider"></div> type="button">
</div> <div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
</div> <div
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
<div class="col-sm-1 btn-group"> </button>
<button (click)="markForOutboundPatternDeletion(i)" class="btn btn-outline-dark trash-button" type="button"> </div>
<i class="fas fa-trash"></i>
</button>
<button (click)="unmarkForOutboundPatternDeletion(i)" *ngIf="markedForDeletionOutboundPattern.includes(i)" class="btn btn-outline-dark undo-button"
type="button">
<i class="fas fa-undo"></i>
</button>
</div>
</div> </div>
</ng-container> </div>
</div>
<span
(click)="addOutboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}
</span>
<div class="mb-5">
&nbsp;
</div>
<div aria-label="Basic example" class="submission-form-footer mt-1 mb-1 position-sticky" role="group">
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button>
<div class="d-flex">
<button (click)="this.openResetFormModal(this.resetFormModal)" class="btn btn-danger" type="button">
<span><i class="fas fa-trash"></i> {{ 'submission.general.discard.submit' | translate }}</span>
</button>
</div> </div>
</div>
<div class="col">
<ng-container
*ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][i].value.pattern)">
<div #outboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}"
ngbDropdown
placement="bottom-start">
<div class="position-relative right-addon" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="outboundItemfilterDropdown.open();"
[readonly]="true"
[value]="selectedOutboundItemfilters"
class="form-control w-100 scrollable-dropdown-input"
formControlName="constraint"
id="outboundItemfilterDropdown"
ngbDropdownAnchor
type="text"
/>
<div aria-labelledby="outboundItemfilterDropdownButton"
class="dropdown-menu scrollable-dropdown-menu w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
<button (click)="selectOutboundItemFilter(constraint.id, i); $event.stopPropagation()"
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem
type="button">
<div>{{ constraint.id }}</div>
</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
<div [style.visibility]="'hidden'" class="col-sm-1">
<input hidden id="automatic{{i}}" name="automatic{{i}}" type="checkbox">
<div
class="toggle-switch">
<div class="slider"></div>
</div>
</div>
<div class="col-sm-2">
<div class="btn-group">
<button (click)="markForOutboundPatternDeletion(i)"
class="btn btn-outline-dark trash-button" type="button">
<i class="fas fa-trash"></i>
</button>
<button (click)="unmarkForOutboundPatternDeletion(i)"
*ngIf="markedForDeletionOutboundPattern.includes(i)"
class="btn btn-warning "
type="button">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div> </div>
</form> </ng-container>
</div>
<div
(click)="addOutboundPattern()"
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}
</div>
<div class="submission-form-footer my-1 position-sticky d-flex justify-content-between" role="group">
<button (click)="openResetFormModal(resetFormModal)" class="btn btn-danger" type="button">
<span><i class="fas fa-trash"></i>&nbsp;{{ 'submission.general.discard.submit' | translate }}</span>
</button>
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i>&nbsp;{{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button>
</div>
</form>
</div> </div>
<ng-template #confirmModal> <ng-template #confirmModal>
<div class="modal-header">
<div>
<h4>{{'service.overview.edit.modal' | translate }}</h4>
</div>
<button (click)="closeModal()" aria-label="Close"
class="close" type="button">
<span aria-hidden="true">×</span>
</button>
</div>
<div> <div class="modal-body">
<div>
<div class="modal-header"> {{ 'service.overview.edit.body' | translate }}
<div> </div>
<h4>{{'service.overview.edit.modal' | translate }}</h4> <div class="mt-4">
</div> <button (click)="closeModal()" class="btn btn-danger mr-2"
<button (click)="closeModal()" aria-label="Close" id="delete-confirm">{{ 'service.detail.return' | translate }}
class="close" type="button"> </button>
<span aria-hidden="true">×</span> <button (click)="patchService()"
</button> class="btn btn-primary">{{ 'service.detail.update' | translate }}
</div> </button>
</div>
<div class="modal-body">
<div>
{{ 'service.overview.edit.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()" class="btn btn-danger mr-2"
id="delete-confirm">{{ 'service.detail.return' | translate }}
</button>
<button (click)="this.patchService()"
class="btn btn-primary custom-btn">{{ 'service.detail.update' | translate }}
</button>
</div>
</div>
</div> </div>
</ng-template> </ng-template>
<ng-template #resetFormModal> <ng-template #resetFormModal>
<div> <div>
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h4>{{'service.overview.reset-form.modal' | translate }}</h4> <h4>{{'service.overview.reset-form.modal' | translate }}</h4>
</div> </div>
<button (click)="closeModal()" aria-label="Close" <button (click)="closeModal()" aria-label="Close"
class="close" type="button"> class="close" type="button">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</div>
<div class="modal-body">
<div>
{{ 'service.overview.reset-form.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()" class="mr-2 btn btn-danger"
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
</button>
<button (click)="resetFormAndLeave()"
class="btn btn-primary custom-btn"
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
</button>
</div>
</div>
</div> </div>
<div class="modal-body">
<div>
{{ 'service.overview.reset-form.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()" class="mr-2 btn btn-danger"
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
</button>
<button (click)="resetFormAndLeave()"
class="btn btn-primary"
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
</button>
</div>
</div>
</div>
</ng-template> </ng-template>

View File

@@ -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 { form {
max-width: 800px;
font-size: 14px; font-size: 14px;
position: relative; position: relative;
} }
input[type="text"], input[type="text"],
@@ -30,7 +31,6 @@ textarea {
.add-pattern-link { .add-pattern-link {
color: #0048ff; color: #0048ff;
cursor: pointer; cursor: pointer;
margin-left: 10px;
} }
.remove-pattern-link { .remove-pattern-link {
@@ -49,6 +49,12 @@ textarea {
color: #000000; color: #000000;
} }
.error-text {
color: red;
font-size: 0.8em;
margin-top: 5px;
}
.toggle-switch { .toggle-switch {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -95,6 +101,11 @@ textarea {
margin-top: 10px; margin-top: 10px;
} }
.small-text {
font-size: 0.7em;
color: #888;
}
.toggle-switch { .toggle-switch {
cursor: pointer; cursor: pointer;
margin-top: 3px; margin-top: 3px;
@@ -113,11 +124,6 @@ textarea {
margin-left: 5px; margin-left: 5px;
} }
form button.btn.btn-primary[type="submit"] {
position: absolute;
right: 8px;
}
.submission-form-footer { .submission-form-footer {
border-radius: var(--bs-card-border-radius); border-radius: var(--bs-card-border-radius);
bottom: 0; bottom: 0;
@@ -127,7 +133,7 @@ form button.btn.btn-primary[type="submit"] {
} }
.marked-for-deletion { .marked-for-deletion {
background-color: #ffcccc; background-color: lighten($red, 30%);
} }

View File

@@ -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', () => { describe('LdnServiceFormEditComponent', () => {
let component: LdnServiceFormEditComponent; let component: LdnServiceFormEditComponent;
let fixture: ComponentFixture<LdnServiceFormEditComponent>; let fixture: ComponentFixture<LdnServiceFormEditComponent>;
beforeEach(async () => { let ldnServicesService: any;
await TestBed.configureTestingModule({ let ldnItemfiltersService: any;
declarations: [LdnServiceFormEditComponent] let cdRefStub: any;
}) let modalService: any;
.compileComponents();
fixture = TestBed.createComponent(LdnServiceFormEditComponent); const translateServiceStub = {
component = fixture.componentInstance; get: () => of('translated-text'),
fixture.detectChanges(); 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', () => { await TestBed.configureTestingModule({
expect(component).toBeTruthy(); 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();
});
}); });

View File

@@ -1,409 +1,579 @@
import { ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import { ActivatedRoute, Router } from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { LdnServicesService } from '../ldn-services-data/ldn-services-data.service'; import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patterns'; import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns';
import { animate, state, style, transition, trigger } from '@angular/animations'; import {animate, state, style, transition, trigger} from '@angular/animations';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import {NotificationsService} from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import { LdnService } from '../ldn-services-model/ldn-services.model'; import {LdnService} from '../ldn-services-model/ldn-services.model';
import { RemoteData } from 'src/app/core/data/remote-data'; import {RemoteData} from 'src/app/core/data/remote-data';
import { Operation } from 'fast-json-patch'; import {Operation} from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
import { LdnItemfiltersService } from '../ldn-services-data/ldn-itemfilters-data.service'; import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import {PaginatedList} from '../../../core/data/paginated-list.model';
import { Observable } from 'rxjs'; import {Observable} from 'rxjs';
import { PaginationService } from '../../../core/pagination/pagination.service'; import {PaginationService} from '../../../core/pagination/pagination.service';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import {FindListOptions} from '../../../core/data/find-list-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-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({ @Component({
selector: 'ds-ldn-service-form-edit', selector: 'ds-ldn-service-form-edit',
templateUrl: './ldn-service-form-edit.component.html', templateUrl: './ldn-service-form-edit.component.html',
styleUrls: ['./ldn-service-form-edit.component.scss'], styleUrls: ['./ldn-service-form-edit.component.scss'],
animations: [ animations: [
trigger('toggleAnimation', [ trigger('toggleAnimation', [
state('true', style({})), state('true', style({})),
state('false', style({})), state('false', style({})),
transition('true <=> false', animate('300ms ease-in')), transition('true <=> false', animate('300ms ease-in')),
]), ]),
], ],
}) })
export class LdnServiceFormEditComponent implements OnInit { export class LdnServiceFormEditComponent implements OnInit {
formModel: FormGroup; formModel: FormGroup;
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>; @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>;
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>; @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>;
public inboundPatterns: object[] = notifyPatterns; public inboundPatterns: string[] = notifyPatterns;
public outboundPatterns: object[] = notifyPatterns; public outboundPatterns: string[] = notifyPatterns;
itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>; itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>;
config: FindListOptions = Object.assign(new FindListOptions(), { config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20 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; this.setItemfilters();
@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;
constructor( /**
protected ldnServicesService: LdnServicesService, * Sets item filters using LDN item filters service
private ldnItemfiltersService: LdnItemfiltersService, */
private formBuilder: FormBuilder, setItemfilters() {
private router: Router, this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe(
private route: ActivatedRoute, getFirstCompletedRemoteData());
private cdRef: ChangeDetectorRef, }
protected modalService: NgbModal,
private notificationService: NotificationsService,
private translateService: TranslateService,
protected paginationService: PaginationService
) {
this.formModel = this.formBuilder.group({ /**
id: [''], * Fetches LDN service data by ID and updates the form
name: ['', Validators.required], * @param serviceId - The ID of the LDN service
description: ['', Validators.required], */
url: ['', Validators.required], fetchServiceData(serviceId: string): void {
ldnUrl: ['', Validators.required], this.ldnServicesService.findById(serviceId).pipe(
inboundPattern: [''], getFirstCompletedRemoteData()
outboundPattern: [''], ).subscribe(
constraintPattern: [''], (data: RemoteData<LdnService>) => {
enabled: [''], if (data.hasSucceeded) {
notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]), this.service = data.payload;
notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]),
type: LDN_SERVICE.value,
});
}
ngOnInit(): void { this.formModel.patchValue({
this.route.params.subscribe((params) => { id: this.service.id,
this.serviceId = params.serviceId; name: this.service.name,
if (this.serviceId) { description: this.service.description,
this.fetchServiceData(this.serviceId); url: this.service.url,
} score: this.service.score, ldnUrl: this.service.ldnUrl,
}); type: this.service.type,
this.setItemfilters(); enabled: this.service.enabled
} });
this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns', false);
setItemfilters() { this.filterPatternObjectsAndPickLabel('notifyServiceOutboundPatterns', true);
this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe(
getFirstCompletedRemoteData());
}
fetchServiceData(serviceId: string): void {
this.ldnServicesService.findById(serviceId).pipe(
getFirstCompletedRemoteData()
).subscribe(
(data: RemoteData<LdnService>) => {
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);
} }
},
);
}
/**
* 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() { servicesToUse.forEach((patternObj: NotifyServicePattern) => {
const newStatus = !this.formModel.get('enabled').value; 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 = { PatternsArray.push(patternFormGroup);
op: 'replace', this.cdRef.detectChanges();
path: '/enabled', });
value: newStatus,
};
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() { const patchOperation: Operation = {
this.modalRef.close(); op: 'replace',
path: '/enabled',
value: newStatus,
};
this.ldnServicesService.patch(this.service, [patchOperation]).pipe(
getFirstCompletedRemoteData()
).subscribe(
() => {
this.formModel.get('enabled').setValue(newStatus);
this.cdRef.detectChanges(); 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(); * Opens a reset form modal with the specified content
this.deleteMarkedOutboundPatterns(); * @param content - The content to be displayed in the modal
const patchOperations = this.generatePatchOperations(); */
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( this.ldnServicesService.patch(this.service, patchOperations).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe( ).subscribe(
() => { (rd: RemoteData<LdnService>) => {
if (rd.hasSucceeded) {
this.closeModal(); this.closeModal();
this.sendBack(); this.sendBack();
this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'), this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'),
this.translateService.get('admin.registries.services-formats.modify.success.content')); 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();
resetFormAndLeave() {
this.sendBack();
this.closeModal();
}
markForInboundPatternDeletion(index: number) {
if (!this.markedForDeletionInboundPattern.includes(index)) {
this.markedForDeletionInboundPattern.push(index);
} }
} });
}
unmarkForInboundPatternDeletion(index: number) { /**
const i = this.markedForDeletionInboundPattern.indexOf(index); * Resets the form and navigates back to the LDN services page
if (i !== -1) { */
this.markedForDeletionInboundPattern.splice(i, 1); 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) { this.markedForDeletionInboundPattern = [];
if (!this.markedForDeletionOutboundPattern.includes(index)) { }
this.markedForDeletionOutboundPattern.push(index);
/**
* 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) { this.markedForDeletionOutboundPattern = [];
const i = this.markedForDeletionOutboundPattern.indexOf(index); }
if (i !== -1) {
this.markedForDeletionOutboundPattern.splice(i, 1); /**
* 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); * Navigates back to the LDN services page
const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray; */
private sendBack() {
this.router.navigateByUrl('admin/ldn/services');
}
for (const index of this.markedForDeletionInboundPattern) { /**
if (index >= 0 && index < patternsArray.length) { * Creates a form group for outbound patterns
const patternGroup = patternsArray.at(index) as FormGroup; * @returns The form group for outbound patterns
const patternValue = patternGroup.value; */
if (patternValue.isNew) { private createOutboundPatternFormGroup(): FormGroup {
patternsArray.removeAt(index); return this.formBuilder.group({
} else { pattern: '',
this.deletedInboundPatterns.push(index); 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); * Initializes an existing form group for inbound patterns
const patternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray; * @returns The initialized form group for inbound patterns
*/
for (const index of this.markedForDeletionOutboundPattern) { private initializeInboundPatternFormGroup(): FormGroup {
if (index >= 0 && index < patternsArray.length) { return this.formBuilder.group({
const patternGroup = patternsArray.at(index) as FormGroup; pattern: '',
const patternValue = patternGroup.value; patternLabel: '',
if (patternValue.isNew) { constraint: '',
patternsArray.removeAt(index); automatic: '',
} 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: '',
});
}
} }

View File

@@ -1,266 +1,386 @@
<div class="container"> <div class="container">
<form (ngSubmit)="onSubmit()" [formGroup]="formModel"> <form (ngSubmit)="onSubmit()" [formGroup]="formModel">
<div class="d-flex"> <div class="d-flex mb-5">
<h2 class="flex-grow-1">{{ 'ldn-create-service.title' | translate }}</h2> <h2 class="flex-grow-1">{{ 'ldn-create-service.title' | translate }}</h2>
</div>
<!-- In the name section -->
<div class="mb-5">
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label>
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
formControlName="name"
id="name"
name="name"
type="text">
<div *ngIf="formModel.get('name').invalid && formModel.get('name').touched" class="error-text">
{{ 'ldn-new-service.form.error.name' | translate }}
</div>
</div>
<!-- In the description section -->
<div class="mb-5 mt-5 d-flex flex-column">
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label>
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
class="form-control" formControlName="description" id="description" name="description"></textarea>
</div>
<!-- In the url section -->
<div class="mb-5 mt-5">
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label>
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
formControlName="url"
id="url"
name="url"
type="text">
<div *ngIf="formModel.get('url').invalid && formModel.get('url').touched" class="error-text">
{{ 'ldn-new-service.form.error.url' | translate }}
</div>
</div>
<!-- In the ldnUrl section -->
<div class="mb-5 mt-5">
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
formControlName="ldnUrl"
id="ldnUrl"
name="ldnUrl"
type="text">
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" class="error-text">
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
</div>
</div>
<!-- In the score section -->
<div class="mb-2">
<label for="score">{{ 'ldn-new-service.form.label.score' | translate }}</label>
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
id="score"
name="score"
class="form-control"
type="text">
<div *ngIf="formModel.get('score').invalid && formModel.get('score').touched" class="error-text">
{{ 'ldn-new-service.form.error.score' | translate }}
</div>
</div>
<!-- In the Inbound Patterns Labels section -->
<div class="row mb-2 mt-5">
<div class="col">
<label>{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div>
<ng-container *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern)">
<div class="col">
<label>{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div> </div>
<!-- In the name section --> <div class="col-sm-1">
<div class="mb-2"> <label class="">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label>
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" formControlName="name" id="name"
name="name"
type="text">
</div> </div>
</ng-container>
<div class="col-sm-1">
</div>
</div>
<div class="mb-4"> <!-- In the Inbound Patterns section -->
&nbsp; <div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
</div> formGroupName="notifyServiceInboundPatterns">
<!-- In the description section --> <ng-container [formGroupName]="i">
<div class="mb-2 d-flex flex-column">
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label>
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
formControlName="description" id="description" name="description"></textarea>
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the url section -->
<div class="mb-2">
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label>
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" formControlName="url" id="url"
name="url"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the ldnUrl section -->
<div class="mb-2">
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" formControlName="ldnUrl"
id="ldnUrl"
name="ldnUrl"
type="text">
</div>
<div class="mb-4">
&nbsp;
</div>
<!-- In the Inbound Patterns section -->
<div class="row">
<div class="col">
<label>{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div>
<div class="col">
<label class="label-box">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
<div class="col-sm1 ">
<label class="label-box-2">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
</div>
<div class="col-sm-1">
</div>
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<div class="row mb-1"> <div class="row mb-1">
<div class="col"> <div class="col">
<select #inboundPattern formControlName="pattern" id="additionalInboundPattern{{i}}" <div #inboundPatternDropdown="ngbDropdown" class="w-100" id="additionalInboundPattern{{i}}" ngbDropdown
name="additionalInboundPattern{{i}}" required> placement="bottom-start">
<option value="">{{ 'ldn-new-service.form.label.placeholder.inboundPattern' | translate }}</option> <div class="position-relative right-addon" role="combobox">
<option *ngFor="let pattern of inboundPatterns" <i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
[ngValue]="pattern.name">{{ pattern.name }}</option> ngbDropdownToggle>
</select>
</div>
</i>
<div class="col"> <input
<ng-container *ngIf="inboundPattern.value"> (click)="inboundPatternDropdown.open();"
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}"> [readonly]="true"
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option> [value]="selectedInboundPatterns"
<option *ngFor="let itemFilter of (itemfiltersRD$ | async)?.payload?.page" class="form-control w-100 scrollable-dropdown-input"
[value]="itemFilter.id">{{ itemFilter.id }}</option> formControlName="patternLabel"
</select> id="inboundPatternDropdownButton"
</ng-container> ngbDropdownAnchor
</div> type="text"
/>
<div [style.visibility]="inboundPattern.value ? 'visible' : 'hidden'" class="col-sm-1"> <div aria-labelledby="inboundPatternDropdownButton"
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}" class="dropdown-menu scrollable-dropdown-menu w-100 "
type="checkbox"> ngbDropdownMenu>
<div (click)="toggleAutomatic(i)" <div class="scrollable-menu" role="listbox">
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value" <button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
class="toggle-switch"> *ngFor="let pattern of inboundPatterns"
<div class="slider"></div> [title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
</div> class="dropdown-item collection-item text-truncate w-100"
</div> ngbDropdownItem
type="button">
<div class="col-sm-1"> <div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
<button (click)="removeInboundPattern(i)" class="btn btn-outline-dark trash-button"> <div
<i class="fas fa-trash"></i> class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
</button> </button>
</div> </div>
</div> </div>
</ng-container> </div>
</div>
<span (click)="addInboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
<div class="mb-4">
&nbsp;
</div>
<!-- In the Outbound Patterns section -->
<div class="row">
<div class="col">
<label>{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label>
</div> </div>
<div class="col"> </div>
<label class="label-box">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
<div class="col-sm-1 ">
</div>
<div class="col-sm-1 ">
</div>
</div>
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
formGroupName="notifyServiceOutboundPatterns">
<ng-container [formGroupName]="i"> <div class="col">
<ng-container
<!-- Input elements in a separate row --> *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern)">
<div class="row mb-1"> <div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
<div class="col"> placement="bottom-start">
<select #outboundPattern formControlName="pattern" id="additionalOutboundPattern{{i}}" <div class="position-relative right-addon" role="combobox">
name="additionalOutboundPattern{{i}}" <i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
required> ngbDropdownToggle></i>
<option value="">{{ 'ldn-new-service.form.label.placeholder.outboundPattern' | translate }}</option> <input
<option *ngFor="let pattern of outboundPatterns" (click)="inboundItemfilterDropdown.open();"
[ngValue]="pattern.name">{{ pattern.name }}</option> [readonly]="true"
</select> [value]="selectedInboundItemfilters"
</div> class="form-control w-100 scrollable-dropdown-input"
<div class="col"> formControlName="constraint"
<ng-container *ngIf="outboundPattern.value"> id="inboundItemfilterDropdown"
<select formControlName="constraint" id="constraint{{i}}" name="constraint{{i}}"> ngbDropdownAnchor
<option value="">{{ 'ldn-new-service.form.label.placeholder.selectedItemFilter' | translate }}</option> type="text"
<option *ngFor="let itemFilter of (itemfiltersRD$ | async)?.payload?.page" />
[value]="itemFilter.id">{{ itemFilter.id }}</option> <div aria-labelledby="inboundItemfilterDropdownButton"
</select> class="dropdown-menu scrollable-dropdown-menu w-100 "
</ng-container> ngbDropdownMenu>
</div> <div class="scrollable-menu" role="listbox">
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation() "
<div [style.visibility]="'hidden'" class="col-sm1"> *ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
<input hidden id="automatic{{i}}" name="automatic{{i}}" type="checkbox"> class="dropdown-item collection-item text-truncate w-100"
<div ngbDropdownItem
class="toggle-switch"> type="button">
<div class="slider"></div> <div>{{ constraint.id }}</div>
</div> </button>
</div>
<div class="col-sm-1">
<button (click)="removeOutboundPattern(i)" class="btn btn-outline-dark trash-button">
<i class="fas fa-trash"></i>
</button>
</div> </div>
</div>
</div> </div>
</div>
</ng-container> </ng-container>
</div>
</div> <div
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i]?.value?.pattern ? 'visible' : 'hidden'"
class="col-sm-1">
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
type="checkbox">
<div (click)="toggleAutomatic(i)"
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
class="toggle-switch">
<div class="slider"></div>
</div>
</div>
<span (click)="addOutboundPattern()" <div class="col-sm-1">
class="add-pattern-link">{{ 'ldn-new-service.form.label.addPattern' | translate }} <button (click)="removeInboundPattern(i)" class="btn btn-outline-dark trash-button">
</span> <i class="fas fa-trash"></i>
<div class="mb-4">
&nbsp;
</div>
<div aria-label="Basic example" class="submission-form-footer mt-1 mb-1 position-sticky" role="group">
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button> </button>
<div class="d-flex"> </div>
<button (click)="this.openResetFormModal(this.resetFormModal)" class="btn btn-danger" type="button">
<span><i class="fas fa-trash"></i> {{ 'submission.general.discard.submit' | translate }}</span>
</button>
</div>
</div> </div>
</ng-container>
</div>
<span (click)="addInboundPattern()"
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
</form> <!-- In the Outbound Patterns Labels section -->
<div class="row mb-1 mt-5">
<div class="col">
<label>{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label>
</div>
<ng-container *ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][0]?.value?.pattern)">
<div class="col">
<label class="">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
</div>
</ng-container>
<div class="col-sm-2">
</div>
</div>
<!-- In the Outbound Patterns section -->
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
formGroupName="notifyServiceOutboundPatterns">
<ng-container [formGroupName]="i">
<div class="row mb-1">
<div class="col">
<div #outboundPatternDropdown="ngbDropdown" class="w-100" id="additionalOutboundPattern{{i}}"
ngbDropdown
placement="bottom-start">
<div class="position-relative right-addon" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="outboundPatternDropdown.open();"
[readonly]="true"
[value]="selectedOutboundPatterns"
class="form-control w-100 scrollable-dropdown-input"
formControlName="patternLabel"
id="outboundPatternDropdownButton"
ngbDropdownAnchor
type="text"
/>
<div aria-labelledby="outboundPatternDropdownButton"
class="dropdown-menu scrollable-dropdown-menu w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
<button (click)="selectOutboundPattern(pattern, i); $event.stopPropagation()"
*ngFor="let pattern of outboundPatterns"
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem
type="button">
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
<div
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<ng-container
*ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][i].value.pattern)">
<div #outboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}"
ngbDropdown
placement="bottom-start">
<div class="position-relative right-addon" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="outboundItemfilterDropdown.open();"
[readonly]="true"
[value]="selectedOutboundItemfilters"
class="form-control w-100 scrollable-dropdown-input"
formControlName="constraint"
id="outboundItemfilterDropdown"
ngbDropdownAnchor
type="text"
/>
<div aria-labelledby="outboundItemfilterDropdownButton"
class="dropdown-menu scrollable-dropdown-menu w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
<button (click)="selectOutboundItemFilter(constraint.id, i); $event.stopPropagation()"
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem
type="button">
<div>{{ constraint.id }}</div>
</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
<div [style.visibility]="'hidden'" class="col-sm-1">
<input hidden id="automatic{{i}}" name="automatic{{i}}"
type="checkbox">
<div
class="toggle-switch">
<div class="slider"></div>
</div>
</div>
<div class="col-sm-1">
<button (click)="removeOutboundPattern(i)" class="btn btn-outline-dark trash-button">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</ng-container>
</div>
<div (click)="addOutboundPattern()"
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}
</div>
<div class="submission-form-footer my-1 position-sticky d-flex justify-content-between" role="group">
<button (click)="this.openResetFormModal(this.resetFormModal)" class="btn btn-danger" type="button">
<span><i class="fas fa-trash"></i>&nbsp;{{ 'submission.general.discard.submit' | translate }}</span>
</button>
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i>&nbsp;{{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button>
</div>
</form>
</div> </div>
<ng-template #confirmModal> <ng-template #confirmModal>
<div> <div>
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h4>{{'service.overview.create.modal' | translate }}</h4> <h4>{{'service.overview.create.modal' | translate }}</h4>
</div> </div>
<button (click)="closeModal()" aria-label="Close" <button (click)="closeModal()" aria-label="Close"
class="close" type="button"> class="close" type="button">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</div>
<div class="modal-body">
<div>
{{ 'service.create.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()" class="btn btn-danger"
id="delete-confirm">{{ 'service.refuse.create' | translate }}
</button>
<button (click)="createService()"
class="btn btn-primary mr-2 custom-btn">{{ 'service.confirm.create' | translate }}
</button>
</div>
</div>
</div> </div>
<div class="modal-body">
<div>
{{ 'service.overview.create.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()" class="btn btn-danger mr-2 "
id="delete-confirm">{{ 'service.refuse.create' | translate }}
</button>
<button (click)="createService()"
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
</button>
</div>
</div>
</div>
</ng-template> </ng-template>
<ng-template #resetFormModal> <ng-template #resetFormModal>
<div> <div>
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h4>{{'service.create.reset-form.modal' | translate }}</h4> <h4>{{'service.overview.reset-form.modal' | translate }}</h4>
</div> </div>
<button (click)="closeModal()" aria-label="Close" <button (click)="closeModal()" aria-label="Close"
class="close" type="button"> class="close" type="button">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</div>
<div class="modal-body">
<div>
{{ 'service.create.reset-form.body' | translate }}
</div>
<div class="mt-4">
<button (click)="resetFormAndLeave()"
class="btn btn-primary mr-2 custom-btn"
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
</button>
<button (click)="closeModal()" class="btn btn-danger"
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
</button>
</div>
</div>
</div> </div>
<div class="modal-body">
<div>
{{ 'service.overview.reset-form.body' | translate }}
</div>
<div class="mt-4">
<button (click)="resetFormAndLeave()"
class="btn btn-primary mr-2"
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
</button>
<button (click)="closeModal()" class="btn btn-danger"
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
</button>
</div>
</div>
</div>
</ng-template> </ng-template>

View File

@@ -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 { form {
max-width: 800px;
font-size: 14px; font-size: 14px;
position: relative; position: relative;
}
label {
font-weight: bold;
} }
input[type="text"], input[type="text"],
@@ -30,7 +35,6 @@ textarea {
.add-pattern-link { .add-pattern-link {
color: #0048ff; color: #0048ff;
cursor: pointer; cursor: pointer;
margin-left: 10px;
} }
.remove-pattern-link { .remove-pattern-link {
@@ -39,6 +43,11 @@ textarea {
margin-left: 10px; margin-left: 10px;
} }
.small-text {
font-size: 0.7em;
color: #888;
}
.status-checkbox { .status-checkbox {
margin-top: 5px; margin-top: 5px;
} }
@@ -49,6 +58,12 @@ textarea {
color: #000000; color: #000000;
} }
.error-text {
color: red;
font-size: 0.8em;
margin-top: 5px;
}
.toggle-switch { .toggle-switch {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -99,24 +114,6 @@ textarea {
cursor: pointer; 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 { .submission-form-footer {
border-radius: var(--bs-card-border-radius); border-radius: var(--bs-card-border-radius);
bottom: 0; bottom: 0;

View File

@@ -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', () => { describe('LdnServiceFormComponent', () => {
let component: LdnServiceFormComponent; let component: LdnServiceFormComponent;
let fixture: ComponentFixture<LdnServiceFormComponent>; let fixture: ComponentFixture<LdnServiceFormComponent>;
beforeEach(async () => { let ldnServicesService: any;
await TestBed.configureTestingModule({ let ldnItemfiltersService: any;
declarations: [LdnServiceFormComponent] let notificationsService: any;
})
.compileComponents(); 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(() => { ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
fixture = TestBed.createComponent(LdnServiceFormComponent); create: jasmine.createSpy('create'),
component = fixture.componentInstance;
fixture.detectChanges();
}); });
it('should create', () => { notificationsService = jasmine.createSpyObj('notificationsService', {
expect(component).toBeTruthy(); 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();
});
}); });

View File

@@ -1,222 +1,369 @@
import { import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild} from '@angular/core';
ChangeDetectorRef, import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
Component, import {Router} from '@angular/router';
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 {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({ @Component({
selector: 'ds-ldn-service-form', selector: 'ds-ldn-service-form',
templateUrl: './ldn-service-form.component.html', templateUrl: './ldn-service-form.component.html',
styleUrls: ['./ldn-service-form.component.scss'], styleUrls: ['./ldn-service-form.component.scss'],
animations: [ animations: [
trigger('toggleAnimation', [ trigger('toggleAnimation', [
state('true', style({})), state('true', style({})),
state('false', style({})), state('false', style({})),
transition('true <=> false', animate('300ms ease-in')), transition('true <=> false', animate('300ms ease-in')),
]), ]),
], ],
}) })
export class LdnServiceFormComponent implements OnInit { export class LdnServiceFormComponent implements OnInit {
formModel: FormGroup; formModel: FormGroup;
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>; @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>;
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>; @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>;
public inboundPatterns: object[] = notifyPatterns; public inboundPatterns: string[] = notifyPatterns;
public outboundPatterns: object[] = notifyPatterns; public outboundPatterns: string[] = notifyPatterns;
itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>; itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>;
config: FindListOptions = Object.assign(new FindListOptions(), { config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20 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<any> = new EventEmitter();
@Output() cancelForm: EventEmitter<any> = 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<any> = new EventEmitter();
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
private modalRef: any;
constructor( this.formModel.value.notifyServiceOutboundPatterns = this.formModel.value.notifyServiceOutboundPatterns.map((pattern: {
private ldnServicesService: LdnServicesService, pattern: string;
private ldnItemfiltersService: LdnItemfiltersService, patternLabel: string
private formBuilder: FormBuilder, }) => {
private router: Router, const {patternLabel, ...rest} = pattern;
private notificationsService: NotificationsService, return rest;
private translateService: TranslateService, });
private cdRef: ChangeDetectorRef,
protected modalService: NgbModal,
) {
this.formModel = this.formBuilder.group({ const values = this.formModel.value;
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,
});
}
ngOnInit(): void { const ldnServiceData = this.ldnServicesService.create(values);
this.setItemfilters();
} ldnServiceData.pipe(
getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<LdnService>) => {
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<LdnService>) => {
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.sendBack();
this.closeModal(); 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(); * Checks if at least one pattern in the specified form array has a value.
this.cdRef.detectChanges(); *
* @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; * Closes the currently open modal and returns to the services directory..
notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup()); */
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; * Selects an outbound pattern for a specific index in the notifyServiceOutboundPatterns form array.
notifyServiceInboundPatternsArray.removeAt(index); *
} * @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; * Selects an inbound pattern for a specific index in the form array.
notifyServiceOutboundPatternsArray.removeAt(index); *
} * @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({ * Sends the user back to the LDN services list.
pattern: [''], */
constraint: [''], private sendBack() {
}); this.router.navigateByUrl('admin/ldn/services');
} }
private createInboundPatternFormGroup(): FormGroup { /**
return this.formBuilder.group({ * Creates a form group for an outbound pattern in the notifyServiceOutboundPatterns form array.
pattern: [''], *
constraint: [''], * @private
automatic: false * @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),
});
}
} }

View File

@@ -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', () => { describe('LdnServiceNewComponent', () => {
let component: LdnServiceNewComponent; let component: LdnServiceNewComponent;
let fixture: ComponentFixture<LdnServiceNewComponent>; let fixture: ComponentFixture<LdnServiceNewComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [LdnServiceNewComponent] declarations: [LdnServiceNewComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LdnServiceNewComponent); fixture = TestBed.createComponent(LdnServiceNewComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@@ -1,27 +1,9 @@
import { Component, OnInit } from '@angular/core'; import {Component} 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";
@Component({ @Component({
selector: 'ds-ldn-service-new', selector: 'ds-ldn-service-new',
templateUrl: './ldn-service-new.component.html', templateUrl: './ldn-service-new.component.html',
styleUrls: ['./ldn-service-new.component.scss'] styleUrls: ['./ldn-service-new.component.scss']
}) })
export class LdnServiceNewComponent implements OnInit { export class LdnServiceNewComponent {
/**
* Emits preselected process if there is one
*/
ldnService$?: Observable<LdnService>;
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() {
}
} }

View File

@@ -1,69 +1,126 @@
import { LdnService } from '../ldn-services-model/ldn-services.model'; import {LdnService} from '../ldn-services-model/ldn-services.model';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import { RemoteData } from '../../../core/data/remote-data'; import {RemoteData} from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import {PaginatedList} from '../../../core/data/paginated-list.model';
import { Observable, of } from 'rxjs'; import {Observable, of} from 'rxjs';
// Create a mock data object for a single LDN notify service import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils';
export const mockLdnService: LdnService = { export const mockLdnService: LdnService = {
id: 1, uuid: '1',
name: 'Service Name', enabled: false,
description: 'Service Description', score: 0,
url: 'Service URL', id: 1,
ldnUrl: 'Service LDN URL', name: 'Service Name',
notifyServiceInboundPatterns: [ description: 'Service Description',
{ url: 'Service URL',
pattern: 'patternA', ldnUrl: 'Service LDN URL',
constraint: 'itemFilterA', notifyServiceInboundPatterns: [
automatic: false, {
}, pattern: 'patternA',
{ constraint: 'itemFilterA',
pattern: 'patternB', automatic: 'false',
constraint: 'itemFilterB',
automatic: true,
},
],
notifyServiceOutboundPatterns: [
{
pattern: 'patternC',
constraint: 'itemFilterC',
},
],
type: LDN_SERVICE,
_links: {
self: {
href: 'http://localhost/api/ldn/ldnservices/1',
},
}, },
{
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: { export const mockLdnServices: LdnService[] = [{
elementsPerPage: 20, uuid: '1',
totalPages: 1, enabled: false,
totalElements: 1, score: 0,
currentPage: 1, id: 1,
first: undefined, name: 'Service Name',
prev: undefined, description: 'Service Description',
next: undefined, url: 'Service URL',
last: undefined, ldnUrl: 'Service LDN URL',
page: [mockLdnService], notifyServiceInboundPatterns: [
type: LDN_SERVICE, {
self: undefined, pattern: 'patternA',
getPageLength: function () { constraint: 'itemFilterA',
return this.page.length; automatic: 'false',
},
_links: {
self: {
href: 'http://localhost/api/ldn/ldnservices/1',
},
page: [],
},
}, },
hasSucceeded: true, {
msToLive: 0, pattern: 'patternB',
}; constraint: 'itemFilterB',
automatic: 'true',
},
// Create a mock ldnServicesRD$ observable ],
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<RemoteData<PaginatedList<LdnService>>> = of((mockLdnServices as unknown) as RemoteData<PaginatedList<LdnService>>); export const mockLdnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>> = of((mockLdnServices as unknown) as RemoteData<PaginatedList<LdnService>>);

View File

@@ -1,20 +1,20 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { dataService } from '../../../core/data/base/data-service.decorator'; import {dataService} from '../../../core/data/base/data-service.decorator';
import { LDN_SERVICE_CONSTRAINT_FILTERS } from '../ldn-services-model/ldn-service.resource-type'; import {LDN_SERVICE_CONSTRAINT_FILTERS} from '../ldn-services-model/ldn-service.resource-type';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
import { RequestService } from '../../../core/data/request.service'; import {RequestService} from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import {ObjectCacheService} from '../../../core/cache/object-cache.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import {NotificationsService} from '../../../shared/notifications/notifications.service';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import {FindListOptions} from '../../../core/data/find-list-options.model';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs'; import {Observable} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import {RemoteData} from '../../../core/data/remote-data';
import { Itemfilter } from '../ldn-services-model/ldn-service-itemfilters'; import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import {PaginatedList} from '../../../core/data/paginated-list.model';
/** /**
@@ -23,25 +23,39 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
@Injectable() @Injectable()
@dataService(LDN_SERVICE_CONSTRAINT_FILTERS) @dataService(LDN_SERVICE_CONSTRAINT_FILTERS)
export class LdnItemfiltersService extends IdentifiableDataService<Itemfilter> implements FindAllData<Itemfilter> { export class LdnItemfiltersService extends IdentifiableDataService<Itemfilter> implements FindAllData<Itemfilter> {
private findAllData: FindAllDataImpl<Itemfilter>; private findAllData: FindAllDataImpl<Itemfilter>;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
) { ) {
super('itemfilters', requestService, rdbService, objectCache, halService); 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<Itemfilter>[]): Observable<RemoteData<PaginatedList<Itemfilter>>> { /**
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<Itemfilter>[]} linksToFollow - Configurations for following specific links.
* @returns {Observable<RemoteData<PaginatedList<Itemfilter>>>} - An observable of remote data containing a paginated list of itemfilters.
*/
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Itemfilter>[]): Observable<RemoteData<PaginatedList<Itemfilter>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
} }

View File

@@ -1,126 +1,208 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { dataService } from '../../../core/data/base/data-service.decorator'; import {dataService} from '../../../core/data/base/data-service.decorator';
import { LDN_SERVICE } from '../ldn-services-model/ldn-service.resource-type'; import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data'; import {DeleteData, DeleteDataImpl} from '../../../core/data/base/delete-data';
import { RequestService } from '../../../core/data/request.service'; import {RequestService} from '../../../core/data/request.service';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import {ObjectCacheService} from '../../../core/cache/object-cache.service';
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import {NotificationsService} from '../../../shared/notifications/notifications.service';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import {FindListOptions} from '../../../core/data/find-list-options.model';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs'; import {Observable} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import {RemoteData} from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import {PaginatedList} from '../../../core/data/paginated-list.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import {NoContent} from '../../../core/shared/NoContent.model';
import { map, take } from 'rxjs/operators'; import {map, take} from 'rxjs/operators';
import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import {URLCombiner} from '../../../core/url-combiner/url-combiner';
import { MultipartPostRequest } from '../../../core/data/request.models'; import {MultipartPostRequest} from '../../../core/data/request.models';
import { RestRequest } from '../../../core/data/rest-request.model'; 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 {PatchData, PatchDataImpl} from '../../../core/data/base/patch-data';
import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; import {ChangeAnalyzer} from '../../../core/data/change-analyzer';
import { Operation } from 'fast-json-patch'; import {Operation} from 'fast-json-patch';
import { RestRequestMethod } from 'src/app/core/data/rest-request-method'; import {RestRequestMethod} from '../../../core/data/rest-request-method';
import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data'; import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data';
import { ldnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model'; import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model';
import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
import { hasValue } from 'src/app/shared/empty.util'; 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<LdnService>}
* @implements {FindAllData<LdnService>}
* @implements {DeleteData<LdnService>}
* @implements {PatchData<LdnService>}
* @implements {CreateData<LdnService>}
*/ */
@Injectable() @Injectable()
@dataService(LDN_SERVICE) @dataService(LDN_SERVICE)
export class LdnServicesService extends IdentifiableDataService<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService>, PatchData<LdnService>, CreateData<LdnService> { export class LdnServicesService extends IdentifiableDataService<LdnService> implements FindAllData<LdnService>, DeleteData<LdnService>, PatchData<LdnService>, CreateData<LdnService> {
createData: CreateDataImpl<LdnService>; createData: CreateDataImpl<LdnService>;
private findAllData: FindAllDataImpl<LdnService>; private findAllData: FindAllDataImpl<LdnService>;
private deleteData: DeleteDataImpl<LdnService>; private deleteData: DeleteDataImpl<LdnService>;
private patchData: PatchDataImpl<LdnService>; private patchData: PatchDataImpl<LdnService>;
private comparator: ChangeAnalyzer<LdnService>; private comparator: ChangeAnalyzer<LdnService>;
private searchData: SearchDataImpl<LdnService>;
constructor( private findByPatternEndpoint = 'byInboundPattern';
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); constructor(
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); protected requestService: RequestService,
this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); protected rdbService: RemoteDataBuildService,
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); 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<RemoteData<LdnService>> { /**
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<RemoteData<LdnService>>} - Observable containing the result of the creation operation.
*/
create(object: LdnService): Observable<RemoteData<LdnService>> {
return this.createData.create(object);
}
patch(object: LdnService, operations: Operation[]): Observable<RemoteData<LdnService>> { /**
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<RemoteData<LdnService>>} - Observable containing the result of the update operation.
*/
patch(object: LdnService, operations: Operation[]): Observable<RemoteData<LdnService>> {
return this.patchData.patch(object, operations);
}
update(object: LdnService): Observable<RemoteData<LdnService>> { /**
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<RemoteData<LdnService>>} - Observable containing the result of the update operation.
*/
update(object: LdnService): Observable<RemoteData<LdnService>> {
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<Operation[]> { /**
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<Operation[]>} - Observable containing the patch operations.
*/
createPatchFromCache(object: LdnService): Observable<Operation[]> {
return this.patchData.createPatchFromCache(object);
}
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<LdnService>[]): Observable<RemoteData<PaginatedList<LdnService>>> { /**
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<LdnService>[]} linksToFollow - Optional links to follow during the request.
* @returns {Observable<RemoteData<PaginatedList<LdnService>>>} - Observable containing the result of the request.
*/
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<LdnService>[]): Observable<RemoteData<PaginatedList<LdnService>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> { /**
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<LdnService>[]} linksToFollow - Optional links to follow during the request.
* @returns {Observable<RemoteData<PaginatedList<LdnService>>>} - Observable containing the result of the request.
*/
findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<LdnService>[]): Observable<RemoteData<PaginatedList<LdnService>>> {
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<RemoteData<NoContent>> { /**
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<RemoteData<NoContent>>} - Observable containing the result of the deletion operation.
*/
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata);
}
public invoke(serviceName: string, serviceId: string, parameters: ldnServiceConstrain[], files: File[]): Observable<RemoteData<LdnService>> { /**
const requestId = this.requestService.generateRequestId(); * Deletes an LDN service by its HATEOAS link.
this.getBrowseEndpoint().pipe( *
take(1), * @param {string} href - The HATEOAS link of the LDN service to be deleted.
map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'processes', serviceId).toString()), * @param {string[]} [copyVirtualMetadata] - Optional virtual metadata to be copied during the deletion.
map((endpoint: string) => { * @returns {Observable<RemoteData<NoContent>>} - Observable containing the result of the deletion operation.
const body = this.getInvocationFormData(parameters, files); */
return new MultipartPostRequest(requestId, endpoint, body); public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
}) return this.deleteData.deleteByHref(href, copyVirtualMetadata);
).subscribe((request: RestRequest) => this.requestService.send(request)); }
return this.rdbService.buildFromRequestUUID<LdnService>(requestId); public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable<RemoteData<LdnService>> {
} 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<boolean> { return this.rdbService.buildFromRequestUUID<LdnService>(requestId);
return this.findById(scriptName).pipe( }
getFirstCompletedRemoteData(),
map((rd: RemoteData<LdnService>) => {
return hasValue(rd.payload);
}),
);
}
private getInvocationFormData(constrain: ldnServiceConstrain[], files: File[]): FormData { public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
const form: FormData = new FormData(); return this.findById(scriptName).pipe(
form.set('properties', JSON.stringify(constrain)); getFirstCompletedRemoteData(),
files.forEach((file: File) => { map((rd: RemoteData<LdnService>) => {
form.append('file', file); return hasValue(rd.payload);
}); }),
return form; );
} }
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;
}
} }

View File

@@ -1,83 +1,84 @@
<div class="container"> <div class="container">
<div class="d-flex"> <div class="d-flex">
<h2 class="flex-grow-1">{{ 'ldn-registered-services.title' | translate }}</h2> <h2 class="flex-grow-1">{{ 'ldn-registered-services.title' | translate }}</h2>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button class="btn btn-success" routerLink="/admin/ldn/services/new"><i <button class="btn btn-success" routerLink="/admin/ldn/services/new"><i
class="fas fa-plus pr-2"></i>{{ 'process.overview.new' | translate }}</button> class="fas fa-plus pr-2"></i>{{ 'process.overview.new' | translate }}</button>
</div> </div>
<ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0" <ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0"
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements" [collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
[pageInfoState]="(ldnServicesRD$ | async)?.payload" [pageInfoState]="(ldnServicesRD$ | async)?.payload"
[paginationOptions]="pageConfig"> [paginationOptions]="pageConfig">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">{{ 'service.overview.table.name' | translate }}</th> <th scope="col">{{ 'service.overview.table.name' | translate }}</th>
<th scope="col">{{ 'service.overview.table.description' | translate }}</th> <th scope="col">{{ 'service.overview.table.description' | translate }}</th>
<th scope="col">{{ 'service.overview.table.status' | translate }}</th> <th scope="col">{{ 'service.overview.table.status' | translate }}</th>
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th> <th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let ldnService of (ldnServicesRD$ | async)?.payload?.page"> <tr *ngFor="let ldnService of (ldnServicesRD$ | async)?.payload?.page">
<td>{{ ldnService.name }}</td> <td>{{ ldnService.name }}</td>
<td>{{ ldnService.description }}</td> <td>{{ ldnService.description }}</td>
<td> <td>
<span (click)="toggleStatus(ldnService, ldnServicesService)" <span (click)="toggleStatus(ldnService, ldnServicesService)"
[ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }" [title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)" [ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)"
class="status-indicator"> class="status-indicator">
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }} {{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
</span> </span>
</td> </td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<button (click)="selectServiceToDelete(ldnService.id)" class="btn btn-outline-danger"> <button (click)="selectServiceToDelete(ldnService.id)" class="btn btn-outline-danger">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]" <button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
class="btn btn-outline-dark"> class="btn btn-outline-dark">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
</div> </div>
<ng-template #deleteModal> <ng-template #deleteModal>
<div> <div>
<div class="modal-header"> <div class="modal-header">
<div> <div>
<h4>{{'service.overview.delete.header' | translate }}</h4> <h4>{{'service.overview.delete.header' | translate }}</h4>
</div> </div>
<button (click)="closeModal()" aria-label="Close" <button (click)="closeModal()" aria-label="Close"
class="close" type="button"> class="close" type="button">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</div>
<div class="modal-body">
<div>
{{ 'service.overview.delete.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()"
class="btn btn-primary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button>
<button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)"
class="btn btn-danger"
id="delete-confirm">{{ 'service.overview.delete' | translate }}
</button>
</div>
</div>
</div> </div>
<div class="modal-body">
<div>
{{ 'service.overview.delete.body' | translate }}
</div>
<div class="mt-4">
<button (click)="closeModal()"
class="btn btn-primary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button>
<button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)"
class="btn btn-danger"
id="delete-confirm">{{ 'service.overview.delete' | translate }}
</button>
</div>
</div>
</div>
</ng-template> </ng-template>

View File

@@ -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<LdnServicesOverviewComponent>;
let ldnServicesService;
let paginationService;
let modalService: NgbModal;
let notificationsService: NotificationsService;
let translateService: TranslateService;
describe('ServicesDirectoryComponent', () => { const translateServiceStub = {
let component: ServicesDirectoryComponent; get: () => of('translated-text'),
let fixture: ComponentFixture<ServicesDirectoryComponent>; onLangChange: new EventEmitter(),
onTranslationChange: new EventEmitter(),
onDefaultLangChange: new EventEmitter()
};
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ paginationService = new PaginationServiceStub();
declarations: [ServicesDirectoryComponent] ldnServicesService = jasmine.createSpyObj('LdnServicesService', ['findAll', 'delete', 'patch']);
}) await TestBed.configureTestingModule({
.compileComponents(); 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<PaginatedList<LdnService>>);
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(() => { describe('openDeleteModal', () => {
fixture = TestBed.createComponent(ServicesDirectoryComponent); it('should open delete modal', () => {
component = fixture.componentInstance; spyOn(modalService, 'open');
fixture.detectChanges(); component.openDeleteModal(component.deleteModal);
expect(modalService.open).toHaveBeenCalledWith(component.deleteModal);
}); });
});
it('should create', () => { describe('closeModal', () => {
expect(component).toBeTruthy(); 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<PaginatedList<LdnService>> */};
spyOn(component, 'setLdnServices').and.callThrough();
const deleteSpy = ldnServicesService.delete.and.returnValue(of(mockRemoteData as RemoteData<PaginatedList<LdnService>>));
component.selectedServiceId = serviceId;
component.deleteSelected(serviceId, ldnServicesService);
tick();
expect(deleteSpy).toHaveBeenCalledWith(serviceId);
}));
});
}); });

View File

@@ -7,140 +7,170 @@ import {
TemplateRef, TemplateRef,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import {Observable, Subscription} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import {RemoteData} from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import {PaginatedList} from '../../../core/data/paginated-list.model';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import {FindListOptions} from '../../../core/data/find-list-options.model';
import { LdnService } from '../ldn-services-model/ldn-services.model'; import {LdnService} from '../ldn-services-model/ldn-services.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model';
import { map, switchMap } from 'rxjs/operators'; import {map, switchMap} from 'rxjs/operators';
import { LdnServicesService } from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; 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 {PaginationService} from 'src/app/core/pagination/pagination.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import { hasValue } from '../../../shared/empty.util'; import {hasValue} from '../../../shared/empty.util';
import { Operation } from 'fast-json-patch'; import {Operation} from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import {NotificationsService} from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; 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({ @Component({
selector: 'ds-ldn-services-directory', selector: 'ds-ldn-services-directory',
templateUrl: './ldn-services-directory.component.html', templateUrl: './ldn-services-directory.component.html',
styleUrls: ['./ldn-services-directory.component.scss'], styleUrls: ['./ldn-services-directory.component.scss'],
changeDetection: ChangeDetectionStrategy.Default changeDetection: ChangeDetectionStrategy.Default
}) })
export class LdnServicesOverviewComponent implements OnInit, OnDestroy { export class LdnServicesOverviewComponent implements OnInit, OnDestroy {
selectedServiceId: string | number | null = null; selectedServiceId: string | number | null = null;
servicesData: any[] = []; servicesData: any[] = [];
@ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef<any>; @ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef<any>;
ldnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>>; ldnServicesRD$: Observable<RemoteData<PaginatedList<LdnService>>>;
config: FindListOptions = Object.assign(new FindListOptions(), { config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20 elementsPerPage: 20
}); });
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'po', id: 'po',
pageSize: 20 pageSize: 20
}); });
isProcessingSub: Subscription; isProcessingSub: Subscription;
private modalRef: any; modalRef: any;
constructor( constructor(
protected ldnServicesService: LdnServicesService, protected ldnServicesService: LdnServicesService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected modalService: NgbModal, protected modalService: NgbModal,
private cdRef: ChangeDetectorRef, public cdRef: ChangeDetectorRef,
private notificationService: NotificationsService, private notificationService: NotificationsService,
private translateService: TranslateService, 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( * Closes the currently open modal and triggers change detection.
switchMap((config) => this.ldnServicesService.findAll(config, false, false).pipe( */
getFirstCompletedRemoteData() closeModal() {
)) this.modalRef.close();
); this.cdRef.detectChanges();
} }
ngOnDestroy(): void { /**
this.paginationService.clearPagination(this.pageConfig.id); * Sets the selected LDN service ID for deletion and opens the delete confirmation modal.
if (hasValue(this.isProcessingSub)) { *
this.isProcessingSub.unsubscribe(); * @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<LdnService>) => {
if (rd.hasSucceeded) {
this.servicesData = this.servicesData.filter(service => service.id !== serviceId);
this.ldnServicesRD$ = this.ldnServicesRD$.pipe(
map((remoteData: RemoteData<PaginatedList<LdnService>>) => {
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() { const patchOperation: Operation = {
this.modalRef.close(); op: 'replace',
this.cdRef.detectChanges(); path: '/enabled',
} value: newStatus,
};
selectServiceToDelete(serviceId: number) { ldnServicesService.patch(ldnService, [patchOperation]).pipe(getFirstCompletedRemoteData()).subscribe(
this.selectedServiceId = serviceId; (rd: RemoteData<LdnService>) => {
this.openDeleteModal(this.deleteModal); if (rd.hasSucceeded) {
} ldnService.enabled = newStatus;
this.notificationService.success(this.translateService.get('ldn-enable-service.notification.success.title'),
deleteSelected(serviceId: string, ldnServicesService: LdnServicesService): void { this.translateService.get('ldn-enable-service.notification.success.content'));
if (this.selectedServiceId !== null) { } else {
ldnServicesService.delete(serviceId).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<LdnService>) => { ldnService.enabled = originalStatus;
if (rd.hasSucceeded) { this.notificationService.error(this.translateService.get('ldn-enable-service.notification.error.title'),
this.servicesData = this.servicesData.filter(service => service.id !== serviceId); this.translateService.get('ldn-enable-service.notification.error.content'));
this.ldnServicesRD$ = this.ldnServicesRD$.pipe(
map((remoteData: RemoteData<PaginatedList<LdnService>>) => {
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();
}
});
} }
} }
);
}
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<LdnService>) => {
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'));
}
}
);
}
} }

View File

@@ -1,31 +1,31 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import {autoserialize, deserialize, inheritSerialization} from 'cerialize';
import { LDN_SERVICE_CONSTRAINT_FILTER } from './ldn-service.resource-type'; import {LDN_SERVICE_CONSTRAINT_FILTER} from './ldn-service.resource-type';
import { CacheableObject } from '../../../core/cache/cacheable-object.model'; import {CacheableObject} from '../../../core/cache/cacheable-object.model';
import { typedObject } from '../../../core/cache/builders/build-decorators'; import {typedObject} from '../../../core/cache/builders/build-decorators';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
import { ResourceType } from '../../../core/shared/resource-type'; import {ResourceType} from '../../../core/shared/resource-type';
/** A single filter value and its properties. */ /** A single filter value and its properties. */
@typedObject @typedObject
@inheritSerialization(CacheableObject) @inheritSerialization(CacheableObject)
export class Itemfilter extends CacheableObject { export class Itemfilter extends CacheableObject {
static type = LDN_SERVICE_CONSTRAINT_FILTER; static type = LDN_SERVICE_CONSTRAINT_FILTER;
@excludeFromEquals @excludeFromEquals
@autoserialize @autoserialize
type: ResourceType; type: ResourceType;
@autoserialize @autoserialize
id: string; id: string;
@deserialize @deserialize
_links: { _links: {
self: { self: {
href: string; href: string;
};
}; };
};
get self(): string { get self(): string {
return this._links.self.href; return this._links.self.href;
} }
} }

View File

@@ -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 { export class NotifyServicePattern {
@autoserialize @autoserialize
pattern: string; pattern: string;
@autoserialize @autoserialize
constraint: string; constraint: string;
@autoserialize @autoserialize
automatic: string; automatic: string;
} }

View File

@@ -2,7 +2,7 @@
* List of services statuses * List of services statuses
*/ */
export enum LdnServiceStatus { export enum LdnServiceStatus {
UNKOWN, UNKOWN,
DISABLED, DISABLED,
ENABLED, ENABLED,
} }

View File

@@ -1,3 +1,3 @@
export class ldnServiceConstrain { export class LdnServiceConstrain {
void: any; void: any;
} }

View File

@@ -4,7 +4,7 @@
* Needs to be in a separate file to prevent circular * Needs to be in a separate file to prevent circular
* dependencies in webpack. * 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 = new ResourceType('ldnservice');
export const LDN_SERVICE_CONSTRAINT_FILTERS = new ResourceType('itemfilters'); export const LDN_SERVICE_CONSTRAINT_FILTERS = new ResourceType('itemfilters');

View File

@@ -1,57 +1,60 @@
import { ResourceType } from '../../../core/shared/resource-type'; import {ResourceType} from '../../../core/shared/resource-type';
import { CacheableObject } from '../../../core/cache/cacheable-object.model'; import {CacheableObject} from '../../../core/cache/cacheable-object.model';
import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; import {autoserialize, deserialize, deserializeAs, inheritSerialization} from 'cerialize';
import { LDN_SERVICE } from './ldn-service.resource-type'; import {LDN_SERVICE} from './ldn-service.resource-type';
import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
import { typedObject } from '../../../core/cache/builders/build-decorators'; import {typedObject} from '../../../core/cache/builders/build-decorators';
import { NotifyServicePattern } from './ldn-service-patterns.model'; import {NotifyServicePattern} from './ldn-service-patterns.model';
/** An LdnService and its properties. */ /** An LdnService and its properties. */
@typedObject @typedObject
@inheritSerialization(CacheableObject) @inheritSerialization(CacheableObject)
export class LdnService extends CacheableObject { export class LdnService extends CacheableObject {
static type = LDN_SERVICE; static type = LDN_SERVICE;
@excludeFromEquals @excludeFromEquals
@autoserialize @autoserialize
type: ResourceType; type: ResourceType;
@autoserialize @autoserialize
id: number; id: number;
@deserializeAs('id') @deserializeAs('id')
uuid: string; uuid: string;
@autoserialize @autoserialize
name: string; name: string;
@autoserialize @autoserialize
description: string; description: string;
@autoserialize @autoserialize
url: string; url: string;
@autoserialize @autoserialize
enabled: boolean; score: number;
@autoserialize @autoserialize
ldnUrl: string; enabled: boolean;
@autoserialize @autoserialize
notifyServiceInboundPatterns?: NotifyServicePattern[]; ldnUrl: string;
@autoserialize @autoserialize
notifyServiceOutboundPatterns?: NotifyServicePattern[]; notifyServiceInboundPatterns?: NotifyServicePattern[];
@deserialize @autoserialize
_links: { notifyServiceOutboundPatterns?: NotifyServicePattern[];
self: {
href: string; @deserialize
}; _links: {
self: {
href: string;
}; };
};
get self(): string { get self(): string {
return this._links.self.href; return this._links.self.href;
} }
} }

View File

@@ -2,9 +2,9 @@
* List of parameter types used for scripts * List of parameter types used for scripts
*/ */
export enum LdnServiceConstrainType { export enum LdnServiceConstrainType {
STRING = 'String', STRING = 'String',
DATE = 'date', DATE = 'date',
BOOLEAN = 'boolean', BOOLEAN = 'boolean',
FILE = 'InputStream', FILE = 'InputStream',
OUTPUT = 'OutputStream' OUTPUT = 'OutputStream'
} }

View File

@@ -1,69 +1,31 @@
export const notifyPatterns = [ export const notifyPatterns = [
{
name: 'Acknowledge and Accept', 'ack-accept',
description: 'This pattern is used to acknowledge and accept a request (offer). It implies an intention to act on the request.',
category: 'Acknowledgements' 'ack-reject',
},
{ 'ack-tentative-accept',
name: 'Acknowledge and Reject',
description: 'This pattern is used to acknowledge and reject a request (offer). It signifies no further action regarding the request.', 'ack-tentative-reject',
category: 'Acknowledgements'
}, 'announce-endorsement',
{
name: 'Acknowledge and Tentatively Accept', 'announce-ingest',
description: 'This pattern is used to acknowledge and tentatively accept a request (offer). It implies an intention to act, which may change.',
category: 'Acknowledgements' 'announce-relationship',
},
{ 'announce-review',
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.', 'announce-service-result',
category: 'Acknowledgements'
}, 'request-endorsement',
{
name: 'Announce Endorsement', 'request-ingest',
description: 'This pattern is used to announce the existence of an endorsement, referencing the endorsed resource.',
category: 'Announcements' 'request-review',
},
{ 'undo-offer',
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'
}
]; ];

View File

@@ -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();
});
});

View File

@@ -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<any> {
return this.ldnServicesService.findAll().pipe(
map((servicesData) => {
return servicesData;
})
);
}
}

View File

@@ -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();
});
});

View File

@@ -49,6 +49,21 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
showBreadcrumbsFluid: false 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 ], canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`, path: `${QUALITY_ASSURANCE_EDIT_PATH}`,

View File

@@ -7,7 +7,7 @@
<p [innerHTML]="('coar-notify-support-title.content' | translate)"></p> <p [innerHTML]="('coar-notify-support-title.content' | translate)"></p>
<h2>{{ 'coar-notify-support.ldn-inbox.title' | translate }}</h2> <h2>{{ 'coar-notify-support.ldn-inbox.title' | translate }}</h2>
<p [innerHTML]="('coar-notify-support.ldn-inbox.content' | translate).replace('{restApiUrl}', coarRestApiUrl)"></p> <p [innerHTML]="('coar-notify-support.ldn-inbox.content' | translate).replace('{ldnInboxUrl}', (coarRestApiUrl | async)?.join(', '))"></p>
<h2>{{ 'coar-notify-support.message-moderation.title' | translate }}</h2> <h2>{{ 'coar-notify-support.message-moderation.title' | translate }}</h2>
<p [innerHTML]="('coar-notify-support.message-moderation.content' | translate)"></p> <p [innerHTML]="('coar-notify-support.message-moderation.content' | translate)"></p>

View File

@@ -1,14 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotifyInfoComponent } from './notify-info.component'; import { NotifyInfoComponent } from './notify-info.component';
import { NotifyInfoService } from './notify-info.service';
import { TranslateModule } from '@ngx-translate/core';
describe('NotifyInfoComponent', () => { describe('NotifyInfoComponent', () => {
let component: NotifyInfoComponent; let component: NotifyInfoComponent;
let fixture: ComponentFixture<NotifyInfoComponent>; let fixture: ComponentFixture<NotifyInfoComponent>;
let notifyInfoServiceSpy: any;
beforeEach(async () => { beforeEach(async () => {
notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['getCoarLdnLocalInboxUrls']);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ NotifyInfoComponent ] imports: [TranslateModule.forRoot()],
declarations: [ NotifyInfoComponent ],
providers: [
{ provide: NotifyInfoService, useValue: notifyInfoServiceSpy }
]
}) })
.compileComponents(); .compileComponents();
}); });

View File

@@ -1,26 +1,24 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { NotifyInfoService } from './notify-info.service'; import { NotifyInfoService } from './notify-info.service';
import { Observable, of } from 'rxjs';
@Component({ @Component({
selector: 'ds-notify-info', selector: 'ds-notify-info',
templateUrl: './notify-info.component.html', templateUrl: './notify-info.component.html',
styleUrls: ['./notify-info.component.scss'] styleUrls: ['./notify-info.component.scss'],
}) })
/**
* Component for displaying COAR notification information.
*/
export class NotifyInfoComponent implements OnInit { export class NotifyInfoComponent implements OnInit {
coarLdnEnabled: boolean; /**
coarRestApiUrl: string; * Observable containing the COAR REST INBOX API URLs.
*/
coarRestApiUrl: Observable<string[]> = of([]);
constructor( constructor(private notifyInfoService: NotifyInfoService) {}
public notifyInfoService: NotifyInfoService,
) {
}
ngOnInit() { ngOnInit() {
this.coarRestApiUrl = this.notifyInfoService.getCoarLdnRestApiUrl(); this.coarRestApiUrl = this.notifyInfoService.getCoarLdnLocalInboxUrls();
}
this.notifyInfoService.isCoarConfigEnabled().subscribe(value => {
this.coarLdnEnabled = value;
});
}
} }

View File

@@ -1,16 +1,49 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { NotifyInfoGuard } from './notify-info.guard'; import { NotifyInfoGuard } from './notify-info.guard';
import { Router } from '@angular/router';
import { NotifyInfoService } from './notify-info.service';
import { of } from 'rxjs';
describe('NotifyInfoGuard', () => { describe('NotifyInfoGuard', () => {
let guard: NotifyInfoGuard; let guard: NotifyInfoGuard;
let notifyInfoServiceSpy: any;
let router: any;
beforeEach(() => { 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); guard = TestBed.inject(NotifyInfoGuard);
}); });
it('should be created', () => { it('should be created', () => {
expect(guard).toBeTruthy(); 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();
});
});
}); });

View File

@@ -16,7 +16,7 @@ export class NotifyInfoGuard implements CanActivate {
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { ): Observable<boolean | UrlTree> {
return this.notifyInfoService.isCoarConfigEnabled().pipe( return this.notifyInfoService.isCoarConfigEnabled().pipe(
map(coarLdnEnabled => { map(coarLdnEnabled => {
if (coarLdnEnabled) { if (coarLdnEnabled) {

View File

@@ -1,16 +1,50 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { NotifyInfoService } from './notify-info.service'; import { NotifyInfoService } from './notify-info.service';
import { ConfigurationDataService } from '../../data/configuration-data.service';
import { of } from 'rxjs';
describe('NotifyInfoService', () => { describe('NotifyInfoService', () => {
let service: NotifyInfoService; let service: NotifyInfoService;
let configurationDataService: any;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); configurationDataService = {
findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(of({})),
};
TestBed.configureTestingModule({
providers: [
NotifyInfoService,
{ provide: ConfigurationDataService, useValue: configurationDataService },
]
});
service = TestBed.inject(NotifyInfoService); service = TestBed.inject(NotifyInfoService);
configurationDataService = TestBed.inject(ConfigurationDataService);
}); });
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); 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');
});
}); });

View File

@@ -1,14 +1,19 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { getFirstSucceededRemoteData } from '../../shared/operators'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators';
import { ConfigurationDataService } from '../../data/configuration-data.service'; import { ConfigurationDataService } from '../../data/configuration-data.service';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { DefaultAppConfig } from '../../../../config/default-app-config'; import { ConfigurationProperty } from '../../shared/configuration-property.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class NotifyInfoService { export class NotifyInfoService {
/**
* The relation link for the inbox
*/
private _inboxRelationLink = 'http://www.w3.org/ns/ldp#inbox';
constructor( constructor(
private configService: ConfigurationDataService, private configService: ConfigurationDataService,
) {} ) {}
@@ -24,15 +29,25 @@ export class NotifyInfoService {
); );
} }
getCoarLdnRestApiUrl(): string { /**
const appConfig = new DefaultAppConfig(); * Get the url of the local inbox from the REST configuration
const restConfig = appConfig.rest; * @returns the url of the local inbox
*/
getCoarLdnLocalInboxUrls(): Observable<string[]> {
return this.configService.findByPropertyName('ldn.notify.inbox').pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((response: ConfigurationProperty) => {
return response.values;
})
);
}
const ssl = restConfig.ssl; /**
const host = restConfig.host; * Method to get the relation link for the inbox
const port = restConfig.port; * @returns the relation link for the inbox
const namespace = restConfig.nameSpace; */
getInboxRelationLink(): string {
return `${ssl ? 'https' : 'http'}://${host}:${port}${namespace}`; return this._inboxRelationLink;
} }
} }

View File

@@ -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 { 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 { 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 { 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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -315,7 +319,8 @@ const PROVIDERS = [
OrcidHistoryDataService, OrcidHistoryDataService,
SupervisionOrderDataService, SupervisionOrderDataService,
LdnServicesService, LdnServicesService,
LdnItemfiltersService LdnItemfiltersService,
CoarNotifyConfigDataService
]; ];
/** /**
@@ -398,7 +403,8 @@ export const models =
SuggestionTarget, SuggestionTarget,
SuggestionSource, SuggestionSource,
LdnService, LdnService,
Itemfilter Itemfilter,
SubmissionCoarNotifyConfig
]; ];

View File

@@ -25,7 +25,6 @@ import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheEntry } from '../cache/object-cache.reducer'; import { ObjectCacheEntry } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';

View File

@@ -26,3 +26,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionSherpaPoliciesObject | WorkspaceitemSectionSherpaPoliciesObject
| WorkspaceitemSectionIdentifiersObject | WorkspaceitemSectionIdentifiersObject
| string; | string;

View File

@@ -19,7 +19,6 @@ import { Item } from '../shared/item.model';
import { WorkspaceItem } from './models/workspaceitem.model'; import { WorkspaceItem } from './models/workspaceitem.model';
import { RequestEntry } from '../data/request-entry.model'; import { RequestEntry } from '../data/request-entry.model';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { testSearchDataImplementation } from '../data/base/search-data.spec';
import { testDeleteDataImplementation } from '../data/base/delete-data.spec'; import { testDeleteDataImplementation } from '../data/base/delete-data.spec';
describe('WorkspaceitemDataService test', () => { describe('WorkspaceitemDataService test', () => {
@@ -84,17 +83,19 @@ describe('WorkspaceitemDataService test', () => {
function initTestService() { function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService(); hrefOnlyDataService = getMockHrefOnlyDataService();
return new WorkspaceitemDataService( return new WorkspaceitemDataService(
comparator,
halService,
http,
notificationsService,
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache,
halService, store,
notificationsService,
); );
} }
describe('composition', () => { describe('composition', () => {
const initService = () => new WorkspaceitemDataService(null, null, null, null, null); const initService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null);
testSearchDataImplementation(initService);
testDeleteDataImplementation(initService); testDeleteDataImplementation(initService);
}); });
@@ -126,7 +127,7 @@ describe('WorkspaceitemDataService test', () => {
service = initTestService(); service = initTestService();
spyOn((service as any), 'findByHref').and.callThrough(); spyOn((service as any), 'findByHref').and.callThrough();
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$); spyOn((service as any), 'getIDHref').and.callThrough();
}); });
afterEach(() => { afterEach(() => {
@@ -138,7 +139,7 @@ describe('WorkspaceitemDataService test', () => {
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo)); scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
scheduler.flush(); scheduler.flush();
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true); expect((service as any).findByHref).toHaveBeenCalled();
}); });
it('should return a RemoteData<WorkspaceItem> for the search', () => { it('should return a RemoteData<WorkspaceItem> for the search', () => {

View File

@@ -50,6 +50,20 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> { public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata); 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<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
/** /**
* Return the WorkspaceItem object found through the UUID of an item * Return the WorkspaceItem object found through the UUID of an item
* *

View File

@@ -84,6 +84,16 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService<Qu
return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow); return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow);
} }
/**
* Service for retrieving Quality Assurance events by topic and target.
* @param options (Optional) The search options to use when retrieving the events.
* @param linksToFollow (Optional) The links to follow when retrieving the events.
* @returns An observable of the remote data containing the paginated list of Quality Assurance events.
*/
public searchEventsByTopic(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<QualityAssuranceEventObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceEventObject>>> {
return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow);
}
/** /**
* Clear findByTopic requests from cache * Clear findByTopic requests from cache
*/ */

View File

@@ -16,6 +16,7 @@ import { PaginatedList } from '../../../data/paginated-list.model';
import { FindListOptions } from '../../../data/find-list-options.model'; import { FindListOptions } from '../../../data/find-list-options.model';
import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; 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. * 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<QualityAssuranceSourceObject> { export class QualityAssuranceSourceDataService extends IdentifiableDataService<QualityAssuranceSourceObject> {
private findAllData: FindAllData<QualityAssuranceSourceObject>; private findAllData: FindAllData<QualityAssuranceSourceObject>;
private searchAllData: SearchData<QualityAssuranceSourceObject>;
private searchByTargetMethod = 'byTarget';
/** /**
* Initialize service variables * Initialize service variables
@@ -43,6 +47,7 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService<Q
) { ) {
super('qualityassurancesources', requestService, rdbService, objectCache, halService); super('qualityassurancesources', 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);
this.searchAllData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
} }
/** /**
@@ -84,4 +89,16 @@ export class QualityAssuranceSourceDataService extends IdentifiableDataService<Q
public getSource(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceSourceObject>[]): Observable<RemoteData<QualityAssuranceSourceObject>> { public getSource(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceSourceObject>[]): Observable<RemoteData<QualityAssuranceSourceObject>> {
return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); 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<QualityAssuranceSourceObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceSourceObject>>> {
return this.searchAllData.searchBy(this.searchByTargetMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
} }

View File

@@ -80,22 +80,53 @@ describe('QualityAssuranceTopicDataService', () => {
notificationsService notificationsService
); );
spyOn((service as any).findAllData, 'findAll').and.callThrough(); spyOn((service as any).searchData, 'searchBy').and.callThrough();
spyOn((service as any), 'findById').and.callThrough(); spyOn((service as any), 'findById').and.callThrough();
}); });
describe('getTopics', () => { describe('searchTopicsByTarget', () => {
it('should call findListByHref', (done) => { it('should call searchData.searchBy with the correct parameters', () => {
service.getTopics().subscribe( const options = { elementsPerPage: 10 };
(res) => { const useCachedVersionIfAvailable = true;
expect((service as any).findAllData.findAll).toHaveBeenCalledWith({}, true, 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<PaginatedList<QualityAssuranceTopicObject>> for the object with the given URL', () => { it('should return a RemoteData<PaginatedList<QualityAssuranceTopicObject>> 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<PaginatedList<QualityAssuranceTopicObject>> for the object with the given URL', () => {
const result = service.searchTopicsBySource();
const expected = cold('(a)', { const expected = cold('(a)', {
a: paginatedListRD a: paginatedListRD
}); });
@@ -121,5 +152,4 @@ describe('QualityAssuranceTopicDataService', () => {
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
}); });
}); });

View File

@@ -15,6 +15,7 @@ import { FindListOptions } from '../../../data/find-list-options.model';
import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service';
import { dataService } from '../../../data/base/data-service.decorator'; import { dataService } from '../../../data/base/data-service.decorator';
import { QUALITY_ASSURANCE_TOPIC_OBJECT } from '../models/quality-assurance-topic-object.resource-type'; 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'; 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<QualityAssuranceTopicObject> { export class QualityAssuranceTopicDataService extends IdentifiableDataService<QualityAssuranceTopicObject> {
private findAllData: FindAllData<QualityAssuranceTopicObject>; private findAllData: FindAllData<QualityAssuranceTopicObject>;
private searchData: SearchData<QualityAssuranceTopicObject>;
private searchByTargetMethod = 'byTarget';
private searchBySourceMethod = 'bySource';
/** /**
* Initialize service variables * Initialize service variables
@@ -43,23 +48,31 @@ export class QualityAssuranceTopicDataService extends IdentifiableDataService<Qu
) { ) {
super('qualityassurancetopics', requestService, rdbService, objectCache, halService); super('qualityassurancetopics', 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);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
} }
/** /**
* Return the list of Quality Assurance topics. * Search for Quality Assurance topics.
* * @param options The search options.
* @param options Find list options object. * @param useCachedVersionIfAvailable Whether to use cached version if available.
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's * @param reRequestOnStale Whether to re-request on stale.
* no valid cached version. Defaults to true * @param linksToFollow The links to follow.
* @param reRequestOnStale Whether or not the request should automatically be re- * @returns An observable of remote data containing a paginated list of Quality Assurance topics.
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
*
* @return Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>>
* The list of Quality Assurance topics.
*/ */
public getTopics(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> { public searchTopicsByTarget(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); 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<QualityAssuranceTopicObject>[]): Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>> {
return this.searchData.searchBy(this.searchBySourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
/** /**

View File

@@ -1,5 +1,5 @@
// ... test imports // ... 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'; 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 { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { of } from 'rxjs';
// Load the implementations that should be tested // Load the implementations that should be tested
import { FooterComponent } from './footer.component'; import { FooterComponent } from './footer.component';
@@ -17,15 +18,21 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { storeModuleConfig } from '../app.reducer'; import { storeModuleConfig } from '../app.reducer';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub'; 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 comp: FooterComponent;
let compAny: any;
let fixture: ComponentFixture<FooterComponent>; let fixture: ComponentFixture<FooterComponent>;
let de: DebugElement; let de: DebugElement;
let el: HTMLElement; let el: HTMLElement;
describe('Footer component', () => { let notifyInfoService = {
isCoarConfigEnabled: () => of(true)
};
// waitForAsync beforeEach describe('Footer component', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({
@@ -38,6 +45,7 @@ describe('Footer component', () => {
providers: [ providers: [
FooterComponent, FooterComponent,
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub }, { provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
{ provide: NotifyInfoService, useValue: notifyInfoService }
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });
@@ -46,9 +54,8 @@ describe('Footer component', () => {
// synchronous beforeEach // synchronous beforeEach
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent); fixture = TestBed.createComponent(FooterComponent);
comp = fixture.componentInstance;
comp = fixture.componentInstance; // component test instance compAny = comp as any;
// query for the title <p> by CSS element selector // query for the title <p> by CSS element selector
de = fixture.debugElement.query(By.css('p')); de = fixture.debugElement.query(By.css('p'));
el = de.nativeElement; el = de.nativeElement;
@@ -59,4 +66,56 @@ describe('Footer component', () => {
expect(app).toBeTruthy(); 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();
}));
});
}); });

View File

@@ -1,22 +1,50 @@
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { map } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Site } from '../core/shared/site.model'; import { Site } from '../core/shared/site.model';
import { environment } from '../../environments/environment'; 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({ @Component({
selector: 'ds-home-page', selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'], styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html' templateUrl: './home-page.component.html'
}) })
export class HomePageComponent implements OnInit { export class HomePageComponent implements OnInit, OnDestroy {
site$: Observable<Site>; site$: Observable<Site>;
recentSubmissionspageSize: number; recentSubmissionspageSize: number;
/**
* An array of LinkDefinition objects representing inbox links for the home page.
*/
inboxLinks: LinkDefinition[] = [];
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private responseService: ServerResponseService,
private notifyInfoService: NotifyInfoService,
protected linkHeadService: LinkHeadService,
@Inject(PLATFORM_ID) private platformId: string
) { ) {
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize; 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 { ngOnInit(): void {
@@ -24,4 +52,38 @@ export class HomePageComponent implements OnInit {
map((data) => data.site as Site), 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}'`);
});
}
} }

View File

@@ -23,6 +23,7 @@ import { RemoteData } from '../../core/data/remote-data';
import { ServerResponseService } from '../../core/services/server-response.service'; import { ServerResponseService } from '../../core/services/server-response.service';
import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkHeadService } from '../../core/services/link-head.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(), { const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -61,6 +62,7 @@ describe('FullItemPageComponent', () => {
let serverResponseService: jasmine.SpyObj<ServerResponseService>; let serverResponseService: jasmine.SpyObj<ServerResponseService>;
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>; let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
let linkHeadService: jasmine.SpyObj<LinkHeadService>; let linkHeadService: jasmine.SpyObj<LinkHeadService>;
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
const mocklink = { const mocklink = {
href: 'http://test.org', href: 'http://test.org',
@@ -105,6 +107,12 @@ describe('FullItemPageComponent', () => {
removeTag: jasmine.createSpy('removeTag'), removeTag: jasmine.createSpy('removeTag'),
}); });
notifyInfoService = jasmine.createSpyObj('NotifyInfoService', {
isCoarConfigEnabled: observableOf(true),
getCoarLdnLocalInboxUrls: observableOf(['http://test.org']),
getInboxRelationLink: observableOf('http://test.org'),
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -122,6 +130,7 @@ describe('FullItemPageComponent', () => {
{ provide: ServerResponseService, useValue: serverResponseService }, { provide: ServerResponseService, useValue: serverResponseService },
{ provide: SignpostingDataService, useValue: signpostingDataService }, { provide: SignpostingDataService, useValue: signpostingDataService },
{ provide: LinkHeadService, useValue: linkHeadService }, { provide: LinkHeadService, useValue: linkHeadService },
{ provide: NotifyInfoService, useValue: notifyInfoService },
{ provide: PLATFORM_ID, useValue: 'server' } { provide: PLATFORM_ID, useValue: 'server' }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -178,7 +187,7 @@ describe('FullItemPageComponent', () => {
it('should add the signposting links', () => { it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); 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', () => { 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', () => { it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); 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', () => { it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); expect(serverResponseService.setHeader).toHaveBeenCalled();
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); expect(linkHeadService.addTag).toHaveBeenCalledTimes(3);
}); });
}); });
}); });

View File

@@ -19,6 +19,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { ServerResponseService } from '../../core/services/server-response.service'; import { ServerResponseService } from '../../core/services/server-response.service';
import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkHeadService } from '../../core/services/link-head.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. * This component renders a full item page.
@@ -55,9 +56,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
protected responseService: ServerResponseService, protected responseService: ServerResponseService,
protected signpostingDataService: SignpostingDataService, protected signpostingDataService: SignpostingDataService,
protected linkHeadService: LinkHeadService, protected linkHeadService: LinkHeadService,
protected notifyInfoService: NotifyInfoService,
@Inject(PLATFORM_ID) protected platformId: string, @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 **/ /*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -60,6 +60,7 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component
import { import {
ThemedFullFileSectionComponent ThemedFullFileSectionComponent
} from './full/field-components/file-section/themed-full-file-section.component'; } 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 = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -103,6 +104,7 @@ const DECLARATIONS = [
ItemAlertsComponent, ItemAlertsComponent,
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
BitstreamRequestACopyPageComponent, BitstreamRequestACopyPageComponent,
QaEventNotificationComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -2,6 +2,7 @@
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut> <div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-themed-item-alerts [item]="item"></ds-themed-item-alerts> <ds-themed-item-alerts [item]="item"></ds-themed-item-alerts>
<ds-qa-event-notification [item]="item"></ds-qa-event-notification>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker> <ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>

View File

@@ -26,6 +26,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi
import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
import { SignpostingLink } from '../../core/data/signposting-links.model'; 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(), { const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -62,6 +63,7 @@ describe('ItemPageComponent', () => {
let serverResponseService: jasmine.SpyObj<ServerResponseService>; let serverResponseService: jasmine.SpyObj<ServerResponseService>;
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>; let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
let linkHeadService: jasmine.SpyObj<LinkHeadService>; let linkHeadService: jasmine.SpyObj<LinkHeadService>;
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
const mockMetadataService = { const mockMetadataService = {
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */ /* eslint-disable no-empty,@typescript-eslint/no-empty-function */
@@ -73,6 +75,8 @@ describe('ItemPageComponent', () => {
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }) data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
}); });
const getCoarLdnLocalInboxUrls = ['http://InboxUrls.org', 'http://InboxUrls2.org'];
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
authService = jasmine.createSpyObj('authService', { authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
@@ -94,6 +98,12 @@ describe('ItemPageComponent', () => {
removeTag: jasmine.createSpy('removeTag'), removeTag: jasmine.createSpy('removeTag'),
}); });
notifyInfoService = jasmine.createSpyObj('NotifyInfoService', {
getInboxRelationLink: 'http://www.w3.org/ns/ldp#inbox',
isCoarConfigEnabled: observableOf(true),
getCoarLdnLocalInboxUrls: observableOf(getCoarLdnLocalInboxUrls),
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -112,6 +122,7 @@ describe('ItemPageComponent', () => {
{ provide: ServerResponseService, useValue: serverResponseService }, { provide: ServerResponseService, useValue: serverResponseService },
{ provide: SignpostingDataService, useValue: signpostingDataService }, { provide: SignpostingDataService, useValue: signpostingDataService },
{ provide: LinkHeadService, useValue: linkHeadService }, { provide: LinkHeadService, useValue: linkHeadService },
{ provide: NotifyInfoService, useValue: notifyInfoService},
{ provide: PLATFORM_ID, useValue: 'server' }, { provide: PLATFORM_ID, useValue: 'server' },
], ],
@@ -166,7 +177,7 @@ describe('ItemPageComponent', () => {
it('should add the signposting links', () => { it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); 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]); expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]);
// Check if linkHeadService.addTag() was called with the correct arguments // 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; let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition;
expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); expect(linkHeadService.addTag).toHaveBeenCalledWith(expected);
expected = { expected = {
@@ -186,8 +197,7 @@ describe('ItemPageComponent', () => {
}); });
it('should set Link header on the server', () => { it('should set Link header on the server', () => {
expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', '<http://test.org> ; rel="rel1" ; type="type1" , <http://test2.org> ; rel="rel2" , <http://InboxUrls.org> ; rel="http://www.w3.org/ns/ldp#inbox", <http://InboxUrls2.org> ; rel="http://www.w3.org/ns/ldp#inbox"');
expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', '<http://test.org> ; rel="rel1" ; type="type1" , <http://test2.org> ; rel="rel2" ');
}); });
}); });
@@ -215,9 +225,9 @@ describe('ItemPageComponent', () => {
expect(objectLoader.nativeElement).toBeDefined(); expect(objectLoader.nativeElement).toBeDefined();
}); });
it('should add the signposting links', () => { it('should add the signposti`ng links`', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); 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', () => { it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled(); expect(serverResponseService.setHeader).toHaveBeenCalled();
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); expect(linkHeadService.addTag).toHaveBeenCalledTimes(4);
}); });
}); });

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { isPlatformServer } from '@angular/common'; import { isPlatformServer } from '@angular/common';
import { Observable } from 'rxjs'; import { Observable, combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data'; 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 { SignpostingLink } from '../../core/data/signposting-links.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; 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. * 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'], styleUrls: ['./item-page.component.scss'],
templateUrl: './item-page.component.html', templateUrl: './item-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut] animations: [fadeInOut],
}) })
export class ItemPageComponent implements OnInit, OnDestroy { export class ItemPageComponent implements OnInit, OnDestroy {
@@ -68,6 +69,13 @@ export class ItemPageComponent implements OnInit, OnDestroy {
*/ */
signpostingLinks: SignpostingLink[] = []; signpostingLinks: SignpostingLink[] = [];
/**
* An array of LinkDefinition objects representing inbox links for the item page.
*/
inboxTags: LinkDefinition[] = [];
coarRestApiUrls: string[] = [];
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected router: Router, protected router: Router,
@@ -77,6 +85,7 @@ export class ItemPageComponent implements OnInit, OnDestroy {
protected responseService: ServerResponseService, protected responseService: ServerResponseService,
protected signpostingDataService: SignpostingDataService, protected signpostingDataService: SignpostingDataService,
protected linkHeadService: LinkHeadService, protected linkHeadService: LinkHeadService,
protected notifyInfoService: NotifyInfoService,
@Inject(PLATFORM_ID) protected platformId: string @Inject(PLATFORM_ID) protected platformId: string
) { ) {
this.initPageLinks(); this.initPageLinks();
@@ -106,7 +115,8 @@ export class ItemPageComponent implements OnInit, OnDestroy {
*/ */
private initPageLinks(): void { private initPageLinks(): void {
this.route.params.subscribe(params => { 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 = ''; let links = '';
this.signpostingLinks = signpostingLinks; this.signpostingLinks = signpostingLinks;
@@ -124,6 +134,11 @@ export class ItemPageComponent implements OnInit, OnDestroy {
this.linkHeadService.addTag(tag); this.linkHeadService.addTag(tag);
}); });
if (coarRestApiUrls.length > 0) {
let inboxLinks = this.initPageInboxLinks(coarRestApiUrls);
links = links + (isNotEmpty(links) ? ', ' : '') + inboxLinks;
}
if (isPlatformServer(this.platformId)) { if (isPlatformServer(this.platformId)) {
this.responseService.setHeader('Link', links); 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<string[]> {
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 { ngOnDestroy(): void {
this.signpostingLinks.forEach((link: SignpostingLink) => { this.signpostingLinks.forEach((link: SignpostingLink) => {
this.linkHeadService.removeTag(`href='${link.href}'`); this.linkHeadService.removeTag(`href='${link.href}'`);
}); });
this.inboxTags.forEach((link: LinkDefinition) => {
this.linkHeadService.removeTag(`href='${link.href}'`);
});
} }
} }

View File

@@ -84,6 +84,18 @@
[label]="'item.page.uri'"> [label]="'item.page.uri'">
</ds-item-page-uri-field> </ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<ds-generic-item-page-field [item]="object"
[fields]="['notify.relation.endorsedBy']"
[label]="'item.page.endorsed-by'">
</ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object"
[fields]="['datacite.relation.isReviewedBy']"
[label]="'item.page.is-reviewed-by'">
</ds-generic-item-page-field>
<ds-generic-item-page-field [item]="object"
[fields]="['datacite.relation.isSupplementedBy']"
[label]="'item.page.is-supplemented-by'">
</ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']">
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}} <i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}

View File

@@ -0,0 +1,14 @@
<ng-container *ngIf="(getQualityAssuranceSources$() | async)?.length > 0">
<ng-container *ngFor="let source of (getQualityAssuranceSources$() | async)">
<div class="alert alert-info d-flex flex-row" *ngIf="source.totalEvents > 0">
<img class="source-logo" src="assets/images/qa-{{(source.id | dsSplit: ':')[0]}}-logo.png" alt="{{source.id}} logo">
<div class="w-100 d-flex justify-content-between">
<div class="pl-4 align-self-center">{{'item.qa-event-notification.check.notification-info' | translate : {num:
source.totalEvents } }} </div>
<button [routerLink]="['/admin/notifications/quality-assurance', (source.id | dsSplit: ':')[0], 'target', item.id]"
class="btn btn-primary align-self-center">{{'item.qa-event-notification-info.check.button' | translate
}}</button>
</div>
</div>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,8 @@
.source-logo {
max-height: var(--ds-header-logo-height);
}
.sections-gap {
gap: 1rem;
}

View File

@@ -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<QaEventNotificationComponent>;
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();
});
});

View File

@@ -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:<target>"
*/
getQualityAssuranceSources$(): Observable<QualityAssuranceSourceObject[]> {
const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', this.item.uuid)]
};
return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions)
.pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
getPaginatedListPayload(),
);
}
}

View File

@@ -1,4 +1,5 @@
<div class="container"> <div class="container">
<ds-my-dspace-qa-events-notifications></ds-my-dspace-qa-events-notifications>
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission> <ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
<ds-suggestions-notification></ds-suggestions-notification> <ds-suggestions-notification></ds-suggestions-notification>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module'; import { SearchModule } from '../shared/search/search.module';
import { UploadModule } from '../shared/upload/upload.module'; import { UploadModule } from '../shared/upload/upload.module';
import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.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 = [ const DECLARATIONS = [
MyDSpacePageComponent, MyDSpacePageComponent,
@@ -23,7 +24,8 @@ const DECLARATIONS = [
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
CollectionSelectorComponent, CollectionSelectorComponent,
MyDSpaceNewSubmissionDropdownComponent, MyDSpaceNewSubmissionDropdownComponent,
MyDSpaceNewExternalDropdownComponent MyDSpaceNewExternalDropdownComponent,
MyDspaceQaEventsNotificationsComponent,
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,28 @@
<ng-container *ngIf="(sources$ | async)?.length > 0">
<ng-container *ngFor="let source of sources$ | async">
<div
class="alert alert-info d-flex flex-row"
*ngIf="source.totalEvents > 0"
>
<img
class="source-logo"
src="assets/images/qa-{{ source.id }}-logo.png"
alt="{{ source.id }} logo"
/>
<div class="w-100 d-flex justify-content-between">
<div class="pl-4 align-self-center">
{{
"mydspace.qa-event-notification.check.notification-info"
| translate : { num: source.totalEvents }
}}
</div>
<button
[routerLink]="['/admin/notifications/quality-assurance', source.id]"
class="btn btn-primary align-self-center"
>
{{ "mydspace.qa-event-notification-info.check.button" | translate }}
</button>
</div>
</div>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,8 @@
.source-logo {
max-height: var(--ds-header-logo-height);
}
.sections-gap {
gap: 1rem;
}

View File

@@ -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<MyDspaceQaEventsNotificationsComponent>;
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();
});
});

View File

@@ -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<QualityAssuranceSourceObject[]> = 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(),
);
}
}

View File

@@ -1838,8 +1838,8 @@ export function getMockNotificationsStateService(): any {
*/ */
export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService { export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService {
return jasmine.createSpyObj('QualityAssuranceTopicDataService', { return jasmine.createSpyObj('QualityAssuranceTopicDataService', {
getTopics: jasmine.createSpy('getTopics'), searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'),
getTopic: jasmine.createSpy('getTopic'), searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'),
}); });
} }

View File

@@ -1,7 +1,6 @@
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { SearchResult } from '../search/models/search-result.model'; import { SearchResult } from '../search/models/search-result.model';
import { SuggestionsService } from '../../suggestion-notifications/reciter-suggestions/suggestions.service';
// REST Mock --------------------------------------------------------------------- // REST Mock ---------------------------------------------------------------------
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------

View File

@@ -284,6 +284,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; } 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 { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination'; import { NgxPaginationModule } from 'ngx-pagination';
import { SplitPipe } from './utils/split.pipe';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,
@@ -323,7 +324,8 @@ const PIPES = [
ObjNgFor, ObjNgFor,
BrowserOnlyPipe, BrowserOnlyPipe,
MarkdownPipe, MarkdownPipe,
ShortNumberPipe ShortNumberPipe,
SplitPipe,
]; ];
const COMPONENTS = [ const COMPONENTS = [

View File

@@ -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);
}
}

View File

@@ -9,7 +9,8 @@
<div class="submission-form-header-item mb-3 mb-sm-0 flex-sm-grow-1 flex-md-grow-0"> <div class="submission-form-header-item mb-3 mb-sm-0 flex-sm-grow-1 flex-md-grow-0">
<ng-container *ngIf="!isSectionHidden"> <ng-container *ngIf="!isSectionHidden">
<ds-submission-form-collection [currentCollectionId]="collectionId" <ds-submission-form-collection
[currentCollectionId]="collectionId"
[currentDefinition]="definitionId" [currentDefinition]="definitionId"
[submissionId]="submissionId" [submissionId]="submissionId"
[collectionModifiable]="collectionModifiable" [collectionModifiable]="collectionModifiable"
@@ -30,10 +31,12 @@
<ng-container *ngFor="let object of (submissionSections | async)"> <ng-container *ngFor="let object of (submissionSections | async)">
<ds-submission-section-container [collectionId]="collectionId" <ds-submission-section-container [collectionId]="collectionId"
[submissionId]="submissionId" [submissionId]="submissionId"
[sectionData]="object"></ds-submission-section-container> [sectionData]="object">
</ds-submission-section-container>
</ng-container> </ng-container>
</div> </div>
<div *ngIf="!(isLoading() | async)" class="submission-form-footer mt-3 mb-3 position-sticky"> <div *ngIf="!(isLoading() | async)" class="submission-form-footer mt-3 mb-3 position-sticky">
<ds-submission-form-footer [submissionId]="submissionId"></ds-submission-form-footer> <ds-submission-form-footer [submissionId]="submissionId"></ds-submission-form-footer>
</div> </div>
</div> </div>

View File

@@ -48,4 +48,4 @@
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
</ngb-accordion> </ngb-accordion>
</div> </div>

View File

@@ -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<SubmissionCoarNotifyConfig> implements FindAllData<SubmissionCoarNotifyConfig>, DeleteData<SubmissionCoarNotifyConfig>, PatchData<SubmissionCoarNotifyConfig>, CreateData<SubmissionCoarNotifyConfig> {
createData: CreateDataImpl<SubmissionCoarNotifyConfig>;
private findAllData: FindAllDataImpl<SubmissionCoarNotifyConfig>;
private deleteData: DeleteDataImpl<SubmissionCoarNotifyConfig>;
private patchData: PatchDataImpl<SubmissionCoarNotifyConfig>;
private comparator: ChangeAnalyzer<SubmissionCoarNotifyConfig>;
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<RemoteData<SubmissionCoarNotifyConfig>> {
return this.createData.create(object);
}
patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
return this.patchData.patch(object, operations);
}
update(object: SubmissionCoarNotifyConfig): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
return this.patchData.update(object);
}
commitUpdates(method?: RestRequestMethod): void {
return this.patchData.commitUpdates(method);
}
createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable<Operation[]> {
return this.patchData.createPatchFromCache(object);
}
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCoarNotifyConfig>[]): Observable<RemoteData<PaginatedList<SubmissionCoarNotifyConfig>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata);
}
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
public invoke(serviceName: string, serviceId: string, files: File[]): Observable<RemoteData<SubmissionCoarNotifyConfig>> {
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<SubmissionCoarNotifyConfig>(requestId);
}
public SubmissionCoarNotifyConfigModelWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
return this.findById(scriptName).pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<SubmissionCoarNotifyConfig>) => {
return hasValue(rd.payload);
}),
);
}
private getInvocationFormData(files: File[]): FormData {
const form: FormData = new FormData();
files.forEach((file: File) => {
form.append('file', file);
});
return form;
}
}

View File

@@ -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');

View File

@@ -0,0 +1,124 @@
<div class="container-fluid">
<ng-container *ngIf="patterns?.length > 0">
<div *ngFor="let pattern of patterns; let i = index" class="col">
<label class="row col-form-label"
>
{{'submission.section.section-coar-notify.control.' + pattern + '.label' | translate }}
</label
>
<div
*ngFor="
let service of ldnServiceByPattern[pattern];
let serviceIndex = index
"
>
<div class="row">
<div ngbDropdown #myDropdown="ngbDropdown" class="w-100">
<div class="position-relative right-addon" role="combobox">
<i ngbDropdownToggle class="position-absolute scrollable-dropdown-toggle"
aria-hidden="true"></i>
<input
type="text"
[readonly]="true"
ngbDropdownAnchor
[ngClass]="{'border-danger': (getShownSectionErrors$(pattern, serviceIndex) | async)?.length > 0}"
class="form-control w-100 scrollable-dropdown-input"
[value]="ldnServiceByPattern[pattern][serviceIndex]?.name"
(click)="myDropdown.open()"
/>
</div>
<div
ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
>
<div
class="scrollable-menu"
role="listbox"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
[scrollWindow]="false"
>
<button
*ngIf="(filterServices(pattern) | async)?.length == 0"
class="dropdown-item collection-item text-truncate w-100"
>
{{'submission.section.section-coar-notify.dropdown.no-data' | translate}}
</button>
<button
*ngIf="(filterServices(pattern) | async)?.length > 0"
class="dropdown-item collection-item text-truncate w-100"
(click)="onChange(pattern, serviceIndex, null)"
>
{{'submission.section.section-coar-notify.dropdown.select-none' | translate}}
</button>
<button
*ngFor="let serviceOption of filterServices(pattern) | async"
[ngClass]="{'bg-light': ldnServiceByPattern[pattern][serviceIndex]?.id == serviceOption.id}"
class="dropdown-item collection-item text-truncate w-100"
(click)="onChange(pattern, serviceIndex, serviceOption)"
>
<b>
{{ serviceOption.name }}
</b>
<br />
{{ serviceOption.description }}
</button>
</div>
</div>
</div>
</div>
<small
class="row text-muted"
*ngIf="!ldnServiceByPattern[pattern][serviceIndex]"
>
{{'submission.section.section-coar-notify.small.notification' | translate : {pattern : pattern} }}
</small>
<ng-container *ngIf="(getShownSectionErrors$(pattern, serviceIndex) | async)?.length > 0">
<small class="row text-danger" *ngFor="let error of (getShownSectionErrors$(pattern, serviceIndex) | async)">
{{ error.message | translate}}
</small>
</ng-container>
<div
class="row mt-1"
*ngIf="ldnServiceByPattern[pattern][serviceIndex]"
>
<div
class="alert alert-info w-100 d-flex align-items-center flex-row"
>
<i class="fa-solid fa-circle-info fa-xl ml-2"></i>
<div class="ml-4">
<div>{{ 'submission.section.section-coar-notify.selection.description' | translate }}</div>
<div *ngIf="ldnServiceByPattern[pattern][serviceIndex]?.description; else noDesc">
{{ ldnServiceByPattern[pattern][serviceIndex].description }}
</div>
<ng-template #noDesc>
<span class="text-muted">
{{ 'submission.section.section-coar-notify.selection.no-description' | translate }}
</span>
</ng-template>
</div>
</div>
</div>
<div class="row" *ngIf="(getShownSectionErrors$(pattern, serviceIndex) | async)?.length > 0">
<div
class="alert alert-danger w-100 d-flex align-items-center flex-row"
>
<div class="ml-4">
<span>
{{ 'submission.section.section-coar-notify.notification.error' | translate }}
</span>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="patterns?.length === 0">
<p>
{{'submission.section.section-coar-notify.info.no-pattern' | translate }}
</p>
</ng-container>
</div>

View File

@@ -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';

View File

@@ -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<SubmissionSectionCoarNotifyComponent>;
let ldnServicesService: jasmine.SpyObj<LdnServicesService>;
let coarNotifyConfigDataService: jasmine.SpyObj<CoarNotifyConfigDataService>;
let operationsBuilder: jasmine.SpyObj<JsonPatchOperationsBuilder>;
let sectionService: jasmine.SpyObj<SectionsService>;
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();
});
});
});

View File

@@ -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<string, number[]> = 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<LdnService[]> {
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<SubmissionSectionError[]> {
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<boolean> {
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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -9,4 +9,5 @@ export enum SectionsType {
SherpaPolicies = 'sherpaPolicy', SherpaPolicies = 'sherpaPolicy',
Identifiers = 'identifiers', Identifiers = 'identifiers',
Collection = 'collection', Collection = 'collection',
CoarNotify = 'coarnotify'
} }

View File

@@ -10,7 +10,7 @@ import { SubmissionFormFooterComponent } from './form/footer/submission-form-foo
import { SubmissionFormComponent } from './form/submission-form.component'; import { SubmissionFormComponent } from './form/submission-form.component';
import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component';
import { SubmissionSectionContainerComponent } from './sections/container/section-container.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 { Action, StoreConfig, StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { submissionReducers, SubmissionState } from './submission.reducers'; import { submissionReducers, SubmissionState } from './submission.reducers';
@@ -67,6 +67,11 @@ import {
} from './sections/sherpa-policies/metadata-information/metadata-information.component'; } from './sections/sherpa-policies/metadata-information/metadata-information.component';
import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; import { SectionFormOperationsService } from './sections/form/section-form-operations.service';
import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component'; 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 = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -76,6 +81,7 @@ const ENTRY_COMPONENTS = [
SubmissionSectionCcLicensesComponent, SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent, SubmissionSectionAccessesComponent,
SubmissionSectionSherpaPoliciesComponent, SubmissionSectionSherpaPoliciesComponent,
SubmissionSectionCoarNotifyComponent
]; ];
const DECLARATIONS = [ const DECLARATIONS = [
@@ -109,20 +115,22 @@ const DECLARATIONS = [
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
CoreModule.forRoot(), CoreModule.forRoot(),
SharedModule, SharedModule,
StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig<SubmissionState, Action>), StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig<SubmissionState, Action>),
EffectsModule.forFeature(submissionEffects), EffectsModule.forFeature(),
JournalEntitiesModule.withEntryComponents(), EffectsModule.forFeature(submissionEffects),
ResearchEntitiesModule.withEntryComponents(), JournalEntitiesModule.withEntryComponents(),
FormModule, ResearchEntitiesModule.withEntryComponents(),
NgbModalModule, FormModule,
NgbCollapseModule, NgbModalModule,
NgbAccordionModule, NgbCollapseModule,
UploadModule, NgbAccordionModule,
], UploadModule,
NgOptimizedImage,
],
declarations: DECLARATIONS, declarations: DECLARATIONS,
exports: [ exports: [
...DECLARATIONS, ...DECLARATIONS,
@@ -135,6 +143,8 @@ const DECLARATIONS = [
SubmissionAccessesConfigDataService, SubmissionAccessesConfigDataService,
SectionAccessesService, SectionAccessesService,
SectionFormOperationsService, SectionFormOperationsService,
CoarNotifyConfigDataService,
LdnServicesService
] ]
}) })

View File

@@ -4,13 +4,19 @@
<h2 class="border-bottom pb-2"> <h2 class="border-bottom pb-2">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
{{'notifications.events.title'| translate}} {{'notifications.events.title'| translate}}
<a class="btn btn-outline-secondary" [routerLink]="['/admin/notifications/quality-assurance']"> <a class="btn btn-outline-secondary" (click)="goBack()">
<i class="fas fa-angle-double-left"></i> <i class="fas fa-angle-double-left"></i>
{{'quality-assurance.events.back' | translate}} {{'quality-assurance.events.back' | translate}}
</a> </a>
</div> </div>
</h2> </h2>
<ds-alert [type]="'alert-info'" [content]="'quality-assurance.events.description'"></ds-alert> <ds-alert *ngIf="!targetId" [type]="'alert-info'">
<span [innerHTML]="'quality-assurance.events.description' | translate : {topic: selectedTopicName, source: sourceId}"></span>
</ds-alert>
<ds-alert *ngIf="targetId" [type]="'alert-info'">
<span [innerHTML]="'quality-assurance.events.description-with-topic-and-target' | translate : {topic: selectedTopicName, source: sourceId}"></span>
<a [routerLink]="itemPageUrl" target="_blank">{{(getTargetItemTitle() | async)}}</a>
</ds-alert>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -54,6 +60,16 @@
target="_blank" target="_blank"
[routerLink]="['/items', eventElement?.target?.id]">{{eventElement.title}}</a> [routerLink]="['/items', eventElement?.target?.id]">{{eventElement.title}}</a>
<span *ngIf="!eventElement?.target">{{eventElement.title}}</span> <span *ngIf="!eventElement?.target">{{eventElement.title}}</span>
<div *ngIf="eventElement?.event?.message?.serviceName">
<span class="small pr-1">{{'quality-assurance.event.table.event.message.serviceName' | translate}}</span>
<span class="badge badge-info">{{eventElement.event.message.serviceName}}</span>
</div>
<div *ngIf="eventElement?.event?.message?.href" class="d-flex align-items-center">
<span class="small pr-1">{{'quality-assurance.event.table.event.message.link' | translate}}</span>
<span [title]="eventElement.event.message.href" class="text-truncate d-inline-block w-75">
<a [href]="eventElement.event.message.href" target="_blank">{{eventElement.event.message.href}}</a>
</span>
</div>
</td> </td>
<td *ngIf="showTopic.indexOf('/PID') !== -1"> <td *ngIf="showTopic.indexOf('/PID') !== -1">
<p><span class="small">{{'quality-assurance.event.table.pidtype' | translate}}</span>&nbsp;<span class="badge badge-info">{{eventElement.event.message.type}}</span></p> <p><span class="small">{{'quality-assurance.event.table.pidtype' | translate}}</span>&nbsp;<span class="badge badge-info">{{eventElement.event.message.type}}</span></p>
@@ -157,7 +173,7 @@
</div> </div>
<div class="row text-right"> <div class="row text-right">
<div class="col-md-12"> <div class="col-md-12">
<a class="btn btn-outline-secondary" [routerLink]="['/admin/notifications/quality-assurance']"> <a class="btn btn-outline-secondary" (click)="goBack()">
<i class="fas fa-angle-double-left"></i> <i class="fas fa-angle-double-left"></i>
{{'quality-assurance.events.back' | translate}} {{'quality-assurance.events.back' | translate}}
</a> </a>

View File

@@ -42,6 +42,7 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { ItemDataService } from 'src/app/core/data/item-data.service';
describe('QualityAssuranceEventsComponent test suite', () => { describe('QualityAssuranceEventsComponent test suite', () => {
let fixture: ComponentFixture<QualityAssuranceEventsComponent>; let fixture: ComponentFixture<QualityAssuranceEventsComponent>;
@@ -118,6 +119,7 @@ describe('QualityAssuranceEventsComponent test suite', () => {
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: TranslateService, useValue: getMockTranslateService() }, { provide: TranslateService, useValue: getMockTranslateService() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: ItemDataService, useValue: {} },
QualityAssuranceEventsComponent QualityAssuranceEventsComponent
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,5 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -26,10 +27,12 @@ import {
ProjectEntryImportModalComponent, ProjectEntryImportModalComponent,
QualityAssuranceEventData QualityAssuranceEventData
} from '../project-entry-import-modal/project-entry-import-modal.component'; } 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 { PaginationService } from '../../../core/pagination/pagination.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { FindListOptions } from '../../../core/data/find-list-options.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. * Component to display the Quality Assurance event list.
@@ -105,6 +108,26 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
*/ */
protected subs: Subscription[] = []; 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. * Initialize the component variables.
* @param {ActivatedRoute} activatedRoute * @param {ActivatedRoute} activatedRoute
@@ -120,7 +143,9 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private qualityAssuranceEventRestService: QualityAssuranceEventDataService, private qualityAssuranceEventRestService: QualityAssuranceEventDataService,
private paginationService: PaginationService, 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; const regEx = /!/g;
this.showTopic = id.replace(regEx, '/'); this.showTopic = id.replace(regEx, '/');
this.topic = id; 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(); return this.getQualityAssuranceEvents();
}) })
).subscribe((events: QualityAssuranceEventData[]) => { ).subscribe((events: QualityAssuranceEventData[]) => {
console.log(events);
this.eventsUpdated$.next(events); this.eventsUpdated$.next(events);
this.isEventPageLoading.next(false); this.isEventPageLoading.next(false);
}); });
@@ -356,7 +384,6 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.totalElements$.next(rd.payload.totalElements); this.totalElements$.next(rd.payload.totalElements);
if (rd.payload.totalElements > 0) { if (rd.payload.totalElements > 0) {
console.log(rd.payload.page);
return this.fetchEvents(rd.payload.page); return this.fetchEvents(rd.payload.page);
} else { } else {
return of([]); return of([]);
@@ -425,4 +452,37 @@ export class QualityAssuranceEventsComponent implements OnInit, OnDestroy {
last() 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<string> {
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();
}
} }

View File

@@ -25,6 +25,8 @@ export class RetrieveAllTopicsAction implements Action {
payload: { payload: {
elementsPerPage: number; elementsPerPage: number;
currentPage: number; currentPage: number;
source: string;
target?: string;
}; };
/** /**
@@ -35,10 +37,12 @@ export class RetrieveAllTopicsAction implements Action {
* @param currentPage * @param currentPage
* The page number to retrieve * The page number to retrieve
*/ */
constructor(elementsPerPage: number, currentPage: number) { constructor(elementsPerPage: number, currentPage: number, source: string, target?: string) {
this.payload = { this.payload = {
elementsPerPage, elementsPerPage,
currentPage currentPage,
source,
target
}; };
} }
} }

View File

@@ -2,7 +2,11 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h2 class="border-bottom pb-2">{{'quality-assurance.title'| translate}}</h2> <h2 class="border-bottom pb-2">{{'quality-assurance.title'| translate}}</h2>
<ds-alert [type]="'alert-info'">{{'quality-assurance.topics.description'| translate:{source: sourceId} }}</ds-alert> <ds-alert *ngIf="!targetId" [type]="'alert-info'">{{'quality-assurance.topics.description'| translate:{source: sourceId} }}</ds-alert>
<ds-alert *ngIf="targetId" [type]="'alert-info'">
{{'quality-assurance.topics.description-with-target'| translate:{source: sourceId} }}
<a [routerLink]="itemPageUrl">{{(getTargetItemTitle() | async)}}</a>
</ds-alert>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -15,7 +19,7 @@
[collectionSize]="(totalElements$ | async)" [collectionSize]="(totalElements$ | async)"
[hideGear]="false" [hideGear]="false"
[hideSortOptions]="true" [hideSortOptions]="true"
(paginationChange)="getQualityAssuranceTopics()"> (paginationChange)="getQualityAssuranceTopics(sourceId, targetId)">
<ds-loading class="container" *ngIf="(isTopicsProcessing() | async)" message="'quality-assurance.loading' | translate"></ds-loading> <ds-loading class="container" *ngIf="(isTopicsProcessing() | async)" message="'quality-assurance.loading' | translate"></ds-loading>
<ng-container *ngIf="!(isTopicsProcessing() | async)"> <ng-container *ngIf="!(isTopicsProcessing() | async)">
@@ -40,7 +44,7 @@
<button <button
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.button.detail' | translate }}" title="{{'quality-assurance.button.detail' | translate }}"
[routerLink]="[topicElement.id]"> [routerLink]="['/admin/notifications/quality-assurance', sourceId, topicElement.id]">
<span class="badge badge-info">{{topicElement.totalEvents}}</span> <span class="badge badge-info">{{topicElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i> <i class="fas fa-info fa-fw"></i>
</button> </button>

View File

@@ -16,7 +16,7 @@ import { SuggestionNotificationsStateService } from '../../suggestion-notificati
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { PaginationService } from '../../../core/pagination/pagination.service'; 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', () => { describe('QualityAssuranceTopicsComponent test suite', () => {
let fixture: ComponentFixture<QualityAssuranceTopicsComponent>; let fixture: ComponentFixture<QualityAssuranceTopicsComponent>;
@@ -44,14 +44,14 @@ describe('QualityAssuranceTopicsComponent test suite', () => {
providers: [ providers: [
{ provide: SuggestionNotificationsStateService, useValue: mockNotificationsStateService }, { provide: SuggestionNotificationsStateService, useValue: mockNotificationsStateService },
{ provide: ActivatedRoute, useValue: { data: observableOf(activatedRouteParams), snapshot: { { provide: ActivatedRoute, useValue: { data: observableOf(activatedRouteParams), snapshot: {
paramMap: { params: {
get: () => 'openaire', sourceId: 'openaire',
targetId: null
}, },
}}}, }}},
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: ItemDataService, useValue: {} },
QualityAssuranceTopicsComponent, QualityAssuranceTopicsComponent,
// tslint:disable-next-line: no-empty
{ provide: QualityAssuranceTopicsService, useValue: { setSourceId: (sourceId: string) => { } }}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents().then(() => { }).compileComponents().then(() => {

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; 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 { SortOptions } from '../../../core/cache/models/sort-options.model';
import { import {
@@ -15,7 +15,10 @@ import {
} from '../../../admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service'; } from '../../../admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
import { ActivatedRoute } from '@angular/router'; 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. * Component to display the Quality Assurance topic list.
@@ -60,6 +63,17 @@ export class QualityAssuranceTopicsComponent implements OnInit {
*/ */
public sourceId: string; 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. * Initialize the component variables.
* @param {PaginationService} paginationService * @param {PaginationService} paginationService
@@ -71,16 +85,16 @@ export class QualityAssuranceTopicsComponent implements OnInit {
private paginationService: PaginationService, private paginationService: PaginationService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private notificationsStateService: SuggestionNotificationsStateService, 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. * Component initialization.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.sourceId = this.activatedRoute.snapshot.paramMap.get('sourceId');
this.qualityAssuranceTopicsService.setSourceId(this.sourceId);
this.topics$ = this.notificationsStateService.getQualityAssuranceTopics(); this.topics$ = this.notificationsStateService.getQualityAssuranceTopics();
this.totalElements$ = this.notificationsStateService.getQualityAssuranceTopicsTotals(); this.totalElements$ = this.notificationsStateService.getQualityAssuranceTopicsTotals();
} }
@@ -93,7 +107,7 @@ export class QualityAssuranceTopicsComponent implements OnInit {
this.notificationsStateService.isQualityAssuranceTopicsLoaded().pipe( this.notificationsStateService.isQualityAssuranceTopicsLoaded().pipe(
take(1) take(1)
).subscribe(() => { ).subscribe(() => {
this.getQualityAssuranceTopics(); this.getQualityAssuranceTopics(this.sourceId, this.targetId);
}) })
); );
} }
@@ -121,13 +135,15 @@ export class QualityAssuranceTopicsComponent implements OnInit {
/** /**
* Dispatch the Quality Assurance topics retrival. * 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( this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
distinctUntilChanged(), distinctUntilChanged(),
).subscribe((options: PaginationComponentOptions) => { ).subscribe((options: PaginationComponentOptions) => {
this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics( this.notificationsStateService.dispatchRetrieveQualityAssuranceTopics(
options.pageSize, 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<string> {
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. * Unsubscribe from all subscriptions.
*/ */

View File

@@ -37,7 +37,9 @@ export class QualityAssuranceTopicsEffects {
switchMap(([action, currentState]: [RetrieveAllTopicsAction, any]) => { switchMap(([action, currentState]: [RetrieveAllTopicsAction, any]) => {
return this.qualityAssuranceTopicService.getTopics( return this.qualityAssuranceTopicService.getTopics(
action.payload.elementsPerPage, action.payload.elementsPerPage,
action.payload.currentPage action.payload.currentPage,
action.payload.source,
action.payload.target
).pipe( ).pipe(
map((topics: PaginatedList<QualityAssuranceTopicObject>) => map((topics: PaginatedList<QualityAssuranceTopicObject>) =>
new AddTopicsAction(topics.page, topics.totalPages, topics.currentPage, topics.totalElements) new AddTopicsAction(topics.page, topics.totalPages, topics.currentPage, topics.totalElements)

View File

@@ -29,7 +29,7 @@ describe('qualityAssuranceTopicsReducer test suite', () => {
const expectedState = qualityAssuranceTopicInitialState; const expectedState = qualityAssuranceTopicInitialState;
expectedState.processing = true; expectedState.processing = true;
const action = new RetrieveAllTopicsAction(elementPerPage, currentPage); const action = new RetrieveAllTopicsAction(elementPerPage, currentPage, 'source', 'target');
const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action); const newState = qualityAssuranceTopicsReducer(qualityAssuranceTopicInitialState, action);
expect(newState).toEqual(expectedState); expect(newState).toEqual(expectedState);

View File

@@ -42,31 +42,46 @@ describe('QualityAssuranceTopicsService', () => {
beforeEach(() => { beforeEach(() => {
restService = TestBed.inject(QualityAssuranceTopicDataService); restService = TestBed.inject(QualityAssuranceTopicDataService);
restServiceAsAny = restService; restServiceAsAny = restService;
restServiceAsAny.getTopics.and.returnValue(observableOf(paginatedListRD)); restServiceAsAny.searchTopicsBySource.and.returnValue(observableOf(paginatedListRD));
restServiceAsAny.searchTopicsByTarget.and.returnValue(observableOf(paginatedListRD));
service = new QualityAssuranceTopicsService(restService); service = new QualityAssuranceTopicsService(restService);
serviceAsAny = service; serviceAsAny = service;
}); });
describe('getTopics', () => { describe('getTopicsBySource', () => {
it('Should proxy the call to qualityAssuranceTopicRestService.getTopics', () => { it('should proxy the call to qualityAssuranceTopicRestService.searchTopicsBySource', () => {
const sortOptions = new SortOptions('name', SortDirection.ASC); const sortOptions = new SortOptions('name', SortDirection.ASC);
const findListOptions: FindListOptions = { const findListOptions: FindListOptions = {
elementsPerPage: elementsPerPage, elementsPerPage: elementsPerPage,
currentPage: currentPage, currentPage: currentPage,
sort: sortOptions, 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, 'openaire');
const result = service.getTopics(elementsPerPage, currentPage); expect((service as any).qualityAssuranceTopicRestService.searchTopicsBySource).toHaveBeenCalledWith(findListOptions);
expect((service as any).qualityAssuranceTopicRestService.getTopics).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|)', { const expected = cold('(a|)', {
a: paginatedList a: paginatedList
}); });
const result = service.getTopics(elementsPerPage, currentPage); const result = service.getTopics(elementsPerPage, currentPage, 'openaire');
expect(result).toBeObservable(expected); 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);
});
}); });
}); });

View File

@@ -13,6 +13,7 @@ import {
import { RequestParam } from '../../../core/cache/models/request-param.model'; import { RequestParam } from '../../../core/cache/models/request-param.model';
import { FindListOptions } from '../../../core/data/find-list-options.model'; import { FindListOptions } from '../../../core/data/find-list-options.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
/** /**
* The service handling all Quality Assurance topic requests to the REST service. * The service handling all Quality Assurance topic requests to the REST service.
@@ -28,10 +29,6 @@ export class QualityAssuranceTopicsService {
private qualityAssuranceTopicRestService: QualityAssuranceTopicDataService private qualityAssuranceTopicRestService: QualityAssuranceTopicDataService
) { } ) { }
/**
* sourceId used to get topics
*/
sourceId: string;
/** /**
* Return the list of Quality Assurance topics managing pagination and errors. * Return the list of Quality Assurance topics managing pagination and errors.
@@ -43,17 +40,25 @@ export class QualityAssuranceTopicsService {
* @return Observable<PaginatedList<QualityAssuranceTopicObject>> * @return Observable<PaginatedList<QualityAssuranceTopicObject>>
* The list of Quality Assurance topics. * The list of Quality Assurance topics.
*/ */
public getTopics(elementsPerPage, currentPage): Observable<PaginatedList<QualityAssuranceTopicObject>> { public getTopics(elementsPerPage, currentPage, source: string, target?: string): Observable<PaginatedList<QualityAssuranceTopicObject>> {
const sortOptions = new SortOptions('name', SortDirection.ASC); const sortOptions = new SortOptions('name', SortDirection.ASC);
const findListOptions: FindListOptions = { const findListOptions: FindListOptions = {
elementsPerPage: elementsPerPage, elementsPerPage: elementsPerPage,
currentPage: currentPage, currentPage: currentPage,
sort: sortOptions, sort: sortOptions,
searchParams: [new RequestParam('source', this.sourceId)] searchParams: [new RequestParam('source', source)]
}; };
return this.qualityAssuranceTopicRestService.getTopics(findListOptions).pipe( let request$: Observable<RemoteData<PaginatedList<QualityAssuranceTopicObject>>>;
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(), getFirstCompletedRemoteData(),
map((rd: RemoteData<PaginatedList<QualityAssuranceTopicObject>>) => { map((rd: RemoteData<PaginatedList<QualityAssuranceTopicObject>>) => {
if (rd.hasSucceeded) { 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;
}
} }

View File

@@ -17,7 +17,8 @@ describe('SuggestionsPopupComponent', () => {
const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', { const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', {
hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'), hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'),
getCurrentUserSuggestionTargets: jasmine.createSpy('getCurrentUserSuggestionTargets'), 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' }; const mockNotificationInterpolation = { count: 12, source: 'source', suggestionId: 'id', displayName: 'displayName' };

View File

@@ -31,7 +31,6 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy {
} }
public initializePopup() { public initializePopup() {
console.log('POPUP INIT dispatchRefreshUserSuggestionsAction');
this.reciterSuggestionStateService.dispatchRefreshUserSuggestionsAction(); this.reciterSuggestionStateService.dispatchRefreshUserSuggestionsAction();
const notifier = new Subject(); const notifier = new Subject();
this.subscription = combineLatest([ this.subscription = combineLatest([

View File

@@ -271,8 +271,8 @@ describe('NotificationsStateService', () => {
it('Should call store.dispatch', () => { it('Should call store.dispatch', () => {
const elementsPerPage = 3; const elementsPerPage = 3;
const currentPage = 1; const currentPage = 1;
const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage); const action = new RetrieveAllTopicsAction(elementsPerPage, currentPage, 'source', 'target');
service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage); service.dispatchRetrieveQualityAssuranceTopics(elementsPerPage, currentPage, 'source', 'target');
expect(serviceAsAny.store.dispatch).toHaveBeenCalledWith(action); expect(serviceAsAny.store.dispatch).toHaveBeenCalledWith(action);
}); });
}); });

View File

@@ -118,8 +118,8 @@ export class SuggestionNotificationsStateService {
* @param currentPage * @param currentPage
* The number of the current page. * The number of the current page.
*/ */
public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number): void { public dispatchRetrieveQualityAssuranceTopics(elementsPerPage: number, currentPage: number, sourceId: string, targteId?: string): void {
this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage)); this.store.dispatch(new RetrieveAllTopicsAction(elementsPerPage, currentPage, sourceId, targteId));
} }
// Quality Assurance source // Quality Assurance source

View File

@@ -5,7 +5,6 @@ import { SuggestionsPageResolver } from './suggestions-page.resolver';
import { SuggestionsPageComponent } from './suggestions-page.component'; import { SuggestionsPageComponent } from './suggestions-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
@NgModule({ @NgModule({
imports: [ imports: [

View File

@@ -892,11 +892,11 @@
"coar-notify-support.title": "COAR Notify Protocol", "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 <a href=\"https://notify.coar-repositories.org/\">here</a>.", "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 <a href=\"https://notify.coar-repositories.org/\">here</a>.",
"coar-notify-support.ldn-inbox.title": "LDN InBox", "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 <code>{restApiUrl}ldn/inbox</code>. 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 <code>{ldnInboxUrl}ldn/inbox</code>. The LDN InBox enables seamless communication and data exchange, ensuring efficient and effective collaboration.",
"coar-notify-support.message-moderation.title": "Message Moderation", "coar-notify-support.message-moderation.title": "Message Moderation",
@@ -905,7 +905,7 @@
"service.overview.delete.header": "Delete Service", "service.overview.delete.header": "Delete Service",
"ldn-registered-services.title": "Registered Services", "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.description": "Description",
"ldn-registered-services.table.status": "Status", "ldn-registered-services.table.status": "Status",
"ldn-registered-services.table.action": "Action", "ldn-registered-services.table.action": "Action",
@@ -920,7 +920,7 @@
"ldn-edit-registered-service.title": "Edit Service", "ldn-edit-registered-service.title": "Edit Service",
"ldn-create-service.title": "Create service", "ldn-create-service.title": "Create service",
"service.overview.create.modal": "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", "ldn-service-status": "Status",
"service.confirm.create": "Create", "service.confirm.create": "Create",
"service.refuse.create": "Discard", "service.refuse.create": "Discard",
@@ -929,16 +929,77 @@
"ldn-new-service.form.label.name": "Name", "ldn-new-service.form.label.name": "Name",
"ldn-new-service.form.label.description": "Description", "ldn-new-service.form.label.description": "Description",
"ldn-new-service.form.label.url": "Service URL", "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.label.ldnUrl": "LDN Inbox URL",
"ldn-new-service.form.placeholder.name": "Please provide service name", "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.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.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.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox",
"ldn-new-service.form.label.inboundPattern": "Inbound Patterns", "ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator",
"ldn-new-service.form.label.placeholder.inboundPattern": "Select an Inbound Pattern", "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.placeholder.selectedItemFilter": "No Item Filter Selected",
"ldn-new-service.form.label.ItemFilter": "Item Filter", "ldn-new-service.form.label.ItemFilter": "Item Filter",
"ldn-new-service.form.label.automatic": "Automatic", "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.outboundPattern": "Outbound Patterns",
"ldn-new-service.form.label.placeholder.outboundPattern": "Select an Outbound Pattern", "ldn-new-service.form.label.placeholder.outboundPattern": "Select an Outbound Pattern",
"ldn-new-service.form.label.addPattern": "+ Add more", "ldn-new-service.form.label.addPattern": "+ Add more",
@@ -951,11 +1012,15 @@
"service.detail.return": "Cancel", "service.detail.return": "Cancel",
"service.overview.reset-form.body": "Are you sure you want to discard those changes and leave?", "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.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.head": "Successful Edit",
"admin.registries.services-formats.modify.success.content": "The service has been edited", "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.title": "Successful Create",
"ldn-service-notification.created.success.body": "The service has been created", "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.title": "Successful status updated",
"ldn-enable-service.notification.success.content": "The service status has been updated", "ldn-enable-service.notification.success.content": "The service status has been updated",
"ldn-service-delete.notification.success.title": "Successful Deletion", "ldn-service-delete.notification.success.title": "Successful Deletion",
@@ -1978,10 +2043,12 @@
"info.feedback.breadcrumbs": "Feedback", "info.feedback.breadcrumbs": "Feedback",
"info.coar-notify-support.title":"Notify Support", "info.coar-notify-support.title": "Notify Support",
"info.coar-notify.breadcrumbs": "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.head": "Feedback",
"info.feedback.title": "Feedback", "info.feedback.title": "Feedback",
@@ -2472,6 +2539,14 @@
"item.truncatable-part.show-less": "Collapse", "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.header": "Delete Supervision Order",
"workflow-item.search.result.delete-supervision.modal.info": "Are you sure you want to 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.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": "Collections",
"item.page.collections.loading": "Loading...", "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": "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.source.description": "Below you can see all the notification's sources.",
"quality-assurance.topics": "Current Topics", "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.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 <b>{{topic}}</b>, related to <b>{{source}}</b>.",
"quality-assurance.events.description-with-topic-and-target": "Below the list of all the suggestions for the selected topic <b>{{topic}}</b>, related to <b>{{source}}</b> and ",
"quality-assurance.loading": "Loading ...", "quality-assurance.loading": "Loading ...",
@@ -3290,6 +3375,10 @@
"quality-assurance.event.table.more": "Show more", "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.found": "Bound to the local record:",
"quality-assurance.event.project.notFound": "No local record found", "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.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.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", "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.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", "submitter.empty": "N/A",
"subscriptions.title": "Subscriptions", "subscriptions.title": "Subscriptions",

View File

@@ -3723,6 +3723,22 @@
// "item.truncatable-part.show-less": "Collapse", // "item.truncatable-part.show-less": "Collapse",
"item.truncatable-part.show-less": "Riduci", "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", // "workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order",
// TODO New key - Add a translation // TODO New key - Add a translation
"workflow-item.search.result.delete-supervision.modal.header": "Delete Supervision Order", "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": "Select this option to view the item's metadata.",
"submission.workspace.generic.view-help": "Seleziona questa opzione per vedere i metadata dell'item.", "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": "Subscriptions",
"subscriptions.title": "Sottoscrizioni", "subscriptions.title": "Sottoscrizioni",

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Some files were not shown because too many files have changed in this diff Show More