;
let de: DebugElement;
let el: HTMLElement;
-describe('Footer component', () => {
+let notifyInfoService = {
+ isCoarConfigEnabled: () => of(true)
+};
- // waitForAsync beforeEach
+describe('Footer component', () => {
beforeEach(waitForAsync(() => {
return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({
@@ -38,6 +45,7 @@ describe('Footer component', () => {
providers: [
FooterComponent,
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
+ { provide: NotifyInfoService, useValue: notifyInfoService }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
@@ -46,9 +54,8 @@ describe('Footer component', () => {
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
-
- comp = fixture.componentInstance; // component test instance
-
+ comp = fixture.componentInstance;
+ compAny = comp as any;
// query for the title by CSS element selector
de = fixture.debugElement.query(By.css('p'));
el = de.nativeElement;
@@ -59,4 +66,56 @@ describe('Footer component', () => {
expect(app).toBeTruthy();
}));
+
+ it('should set showPrivacyPolicy to the value of environment.info.enablePrivacyStatement', () => {
+ expect(comp.showPrivacyPolicy).toBe(environment.info.enablePrivacyStatement);
+ });
+
+ it('should set showEndUserAgreement to the value of environment.info.enableEndUserAgreement', () => {
+ expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
+ });
+
+ describe('showCookieSettings', () => {
+ it('should call cookies.showSettings() if cookies is defined', () => {
+ const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
+ compAny.cookies = cookies;
+ comp.showCookieSettings();
+ expect(cookies.showSettings).toHaveBeenCalled();
+ });
+
+ it('should not call cookies.showSettings() if cookies is undefined', () => {
+ compAny.cookies = undefined;
+ expect(() => comp.showCookieSettings()).not.toThrow();
+ });
+
+ it('should return false', () => {
+ expect(comp.showCookieSettings()).toBeFalse();
+ });
+ });
+
+ describe('when coarLdnEnabled is true', () => {
+ beforeEach(() => {
+ spyOn(notifyInfoService, 'isCoarConfigEnabled').and.returnValue(of(true));
+ fixture.detectChanges();
+ });
+
+ it('should set coarLdnEnabled based on notifyInfoService', () => {
+ expect(comp.coarLdnEnabled).toBeTruthy();
+ // Check if COAR Notify section is rendered
+ const notifySection = fixture.debugElement.query(By.css('.notify-enabled'));
+ expect(notifySection).toBeTruthy();
+ });
+
+ it('should redirect to info/coar-notify-support', () => {
+ // Check if the link to the COAR Notify support page is present
+ const routerLink = fixture.debugElement.query(By.css('a[routerLink="info/coar-notify-support"].coar-notify-support-route'));
+ expect(routerLink).toBeTruthy();
+ });
+
+ it('should have an img tag with the class "n-coar" when coarLdnEnabled is true', fakeAsync(() => {
+ // Check if the img tag with the class "n-coar" is present
+ const imgTag = fixture.debugElement.query(By.css('.notify-enabled img.n-coar'));
+ expect(imgTag).toBeTruthy();
+ }));
+ });
});
diff --git a/src/app/home-page/home-page.component.ts b/src/app/home-page/home-page.component.ts
index c151cbbb16..585fd06fe6 100644
--- a/src/app/home-page/home-page.component.ts
+++ b/src/app/home-page/home-page.component.ts
@@ -1,22 +1,50 @@
-import { Component, OnInit } from '@angular/core';
-import { map } from 'rxjs/operators';
+import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
+import { map, switchMap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { Site } from '../core/shared/site.model';
import { environment } from '../../environments/environment';
+import { isPlatformServer } from '@angular/common';
+import { ServerResponseService } from '../core/services/server-response.service';
+import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
+import { LinkDefinition, LinkHeadService } from '../core/services/link-head.service';
+import { isNotEmpty } from '../shared/empty.util';
+
@Component({
selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html'
})
-export class HomePageComponent implements OnInit {
+export class HomePageComponent implements OnInit, OnDestroy {
site$: Observable;
recentSubmissionspageSize: number;
+ /**
+ * An array of LinkDefinition objects representing inbox links for the home page.
+ */
+ inboxLinks: LinkDefinition[] = [];
+
constructor(
private route: ActivatedRoute,
+ private responseService: ServerResponseService,
+ private notifyInfoService: NotifyInfoService,
+ protected linkHeadService: LinkHeadService,
+ @Inject(PLATFORM_ID) private platformId: string
) {
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;
+ // Get COAR REST API URLs from REST configuration
+ // only if COAR configuration is enabled
+ this.notifyInfoService.isCoarConfigEnabled().pipe(
+ switchMap((coarLdnEnabled: boolean) => {
+ if (coarLdnEnabled) {
+ return this.notifyInfoService.getCoarLdnLocalInboxUrls();
+ }
+ })
+ ).subscribe((coarRestApiUrls: string[]) => {
+ if (coarRestApiUrls.length > 0) {
+ this.initPageLinks(coarRestApiUrls);
+ }
+ });
}
ngOnInit(): void {
@@ -24,4 +52,38 @@ export class HomePageComponent implements OnInit {
map((data) => data.site as Site),
);
}
+
+ /**
+ * Initializes page links for COAR REST API URLs.
+ * @param coarRestApiUrls An array of COAR REST API URLs.
+ */
+ private initPageLinks(coarRestApiUrls: string[]): void {
+ const rel = this.notifyInfoService.getInboxRelationLink();
+ let links = '';
+ coarRestApiUrls.forEach((coarRestApiUrl: string) => {
+ // Add link to head
+ let tag: LinkDefinition = {
+ href: coarRestApiUrl,
+ rel: rel
+ };
+ this.inboxLinks.push(tag);
+ this.linkHeadService.addTag(tag);
+
+ links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`;
+ });
+
+ if (isPlatformServer(this.platformId)) {
+ // Add link to response header
+ this.responseService.setHeader('Link', links);
+ }
+ }
+
+ /**
+ * It removes the inbox links from the head of the html.
+ */
+ ngOnDestroy(): void {
+ this.inboxLinks.forEach((link: LinkDefinition) => {
+ this.linkHeadService.removeTag(`href='${link.href}'`);
+ });
+ }
}
diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts
index 9fc078c2cd..c09c3177e3 100644
--- a/src/app/item-page/full/full-item-page.component.spec.ts
+++ b/src/app/item-page/full/full-item-page.component.spec.ts
@@ -23,6 +23,7 @@ import { RemoteData } from '../../core/data/remote-data';
import { ServerResponseService } from '../../core/services/server-response.service';
import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
+import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -61,6 +62,7 @@ describe('FullItemPageComponent', () => {
let serverResponseService: jasmine.SpyObj;
let signpostingDataService: jasmine.SpyObj;
let linkHeadService: jasmine.SpyObj;
+ let notifyInfoService: jasmine.SpyObj;
const mocklink = {
href: 'http://test.org',
@@ -105,6 +107,12 @@ describe('FullItemPageComponent', () => {
removeTag: jasmine.createSpy('removeTag'),
});
+ notifyInfoService = jasmine.createSpyObj('NotifyInfoService', {
+ isCoarConfigEnabled: observableOf(true),
+ getCoarLdnLocalInboxUrls: observableOf(['http://test.org']),
+ getInboxRelationLink: observableOf('http://test.org'),
+ });
+
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
@@ -122,6 +130,7 @@ describe('FullItemPageComponent', () => {
{ provide: ServerResponseService, useValue: serverResponseService },
{ provide: SignpostingDataService, useValue: signpostingDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: notifyInfoService },
{ provide: PLATFORM_ID, useValue: 'server' }
],
schemas: [NO_ERRORS_SCHEMA]
@@ -178,7 +187,7 @@ describe('FullItemPageComponent', () => {
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(3);
});
});
describe('when the item is withdrawn and the user is not an admin', () => {
@@ -207,7 +216,7 @@ describe('FullItemPageComponent', () => {
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(3);
});
});
@@ -224,7 +233,7 @@ describe('FullItemPageComponent', () => {
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(3);
});
});
});
diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts
index 31dd2c5fc2..09238c30ab 100644
--- a/src/app/item-page/full/full-item-page.component.ts
+++ b/src/app/item-page/full/full-item-page.component.ts
@@ -19,6 +19,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { ServerResponseService } from '../../core/services/server-response.service';
import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
+import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
/**
* This component renders a full item page.
@@ -55,9 +56,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
protected responseService: ServerResponseService,
protected signpostingDataService: SignpostingDataService,
protected linkHeadService: LinkHeadService,
+ protected notifyInfoService: NotifyInfoService,
@Inject(PLATFORM_ID) protected platformId: string,
) {
- super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId);
+ super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, notifyInfoService, platformId);
}
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts
index a8d41d1535..5aa1b6e508 100644
--- a/src/app/item-page/item-page.module.ts
+++ b/src/app/item-page/item-page.module.ts
@@ -60,6 +60,7 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component
import {
ThemedFullFileSectionComponent
} from './full/field-components/file-section/themed-full-file-section.component';
+import { QaEventNotificationComponent } from './simple/qa-event-notification/qa-event-notification.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -103,6 +104,7 @@ const DECLARATIONS = [
ItemAlertsComponent,
ThemedItemAlertsComponent,
BitstreamRequestACopyPageComponent,
+ QaEventNotificationComponent
];
@NgModule({
diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html
index cc9983bb35..37a5e0c4cb 100644
--- a/src/app/item-page/simple/item-page.component.html
+++ b/src/app/item-page/simple/item-page.component.html
@@ -2,6 +2,7 @@
+
diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts
index b3202108f4..433b950cee 100644
--- a/src/app/item-page/simple/item-page.component.spec.ts
+++ b/src/app/item-page/simple/item-page.component.spec.ts
@@ -26,6 +26,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi
import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
import { SignpostingLink } from '../../core/data/signposting-links.model';
+import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -62,6 +63,7 @@ describe('ItemPageComponent', () => {
let serverResponseService: jasmine.SpyObj
;
let signpostingDataService: jasmine.SpyObj;
let linkHeadService: jasmine.SpyObj;
+ let notifyInfoService: jasmine.SpyObj;
const mockMetadataService = {
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
@@ -73,6 +75,8 @@ describe('ItemPageComponent', () => {
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
});
+ const getCoarLdnLocalInboxUrls = ['http://InboxUrls.org', 'http://InboxUrls2.org'];
+
beforeEach(waitForAsync(() => {
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true),
@@ -94,6 +98,12 @@ describe('ItemPageComponent', () => {
removeTag: jasmine.createSpy('removeTag'),
});
+ notifyInfoService = jasmine.createSpyObj('NotifyInfoService', {
+ getInboxRelationLink: 'http://www.w3.org/ns/ldp#inbox',
+ isCoarConfigEnabled: observableOf(true),
+ getCoarLdnLocalInboxUrls: observableOf(getCoarLdnLocalInboxUrls),
+ });
+
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
@@ -112,6 +122,7 @@ describe('ItemPageComponent', () => {
{ provide: ServerResponseService, useValue: serverResponseService },
{ provide: SignpostingDataService, useValue: signpostingDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
+ { provide: NotifyInfoService, useValue: notifyInfoService},
{ provide: PLATFORM_ID, useValue: 'server' },
],
@@ -166,7 +177,7 @@ describe('ItemPageComponent', () => {
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(4);
});
@@ -175,7 +186,7 @@ describe('ItemPageComponent', () => {
expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]);
// Check if linkHeadService.addTag() was called with the correct arguments
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length + getCoarLdnLocalInboxUrls.length);
let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition;
expect(linkHeadService.addTag).toHaveBeenCalledWith(expected);
expected = {
@@ -186,8 +197,7 @@ describe('ItemPageComponent', () => {
});
it('should set Link header on the server', () => {
-
- expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" ');
+ expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" , ; rel="http://www.w3.org/ns/ldp#inbox", ; rel="http://www.w3.org/ns/ldp#inbox"');
});
});
@@ -215,9 +225,9 @@ describe('ItemPageComponent', () => {
expect(objectLoader.nativeElement).toBeDefined();
});
- it('should add the signposting links', () => {
+ it('should add the signposti`ng links`', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(4);
});
});
@@ -234,7 +244,7 @@ describe('ItemPageComponent', () => {
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
- expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
+ expect(linkHeadService.addTag).toHaveBeenCalledTimes(4);
});
});
diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts
index b9be6bebfb..a057e99715 100644
--- a/src/app/item-page/simple/item-page.component.ts
+++ b/src/app/item-page/simple/item-page.component.ts
@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM
import { ActivatedRoute, Router } from '@angular/router';
import { isPlatformServer } from '@angular/common';
-import { Observable } from 'rxjs';
-import { map, take } from 'rxjs/operators';
+import { Observable, combineLatest } from 'rxjs';
+import { map, switchMap, take } from 'rxjs/operators';
import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data';
@@ -21,6 +21,7 @@ import { SignpostingDataService } from '../../core/data/signposting-data.service
import { SignpostingLink } from '../../core/data/signposting-links.model';
import { isNotEmpty } from '../../shared/empty.util';
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
+import { NotifyInfoService } from 'src/app/core/coar-notify/notify-info/notify-info.service';
/**
* This component renders a simple item page.
@@ -32,7 +33,7 @@ import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.s
styleUrls: ['./item-page.component.scss'],
templateUrl: './item-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
- animations: [fadeInOut]
+ animations: [fadeInOut],
})
export class ItemPageComponent implements OnInit, OnDestroy {
@@ -68,6 +69,13 @@ export class ItemPageComponent implements OnInit, OnDestroy {
*/
signpostingLinks: SignpostingLink[] = [];
+ /**
+ * An array of LinkDefinition objects representing inbox links for the item page.
+ */
+ inboxTags: LinkDefinition[] = [];
+
+ coarRestApiUrls: string[] = [];
+
constructor(
protected route: ActivatedRoute,
protected router: Router,
@@ -77,6 +85,7 @@ export class ItemPageComponent implements OnInit, OnDestroy {
protected responseService: ServerResponseService,
protected signpostingDataService: SignpostingDataService,
protected linkHeadService: LinkHeadService,
+ protected notifyInfoService: NotifyInfoService,
@Inject(PLATFORM_ID) protected platformId: string
) {
this.initPageLinks();
@@ -106,7 +115,8 @@ export class ItemPageComponent implements OnInit, OnDestroy {
*/
private initPageLinks(): void {
this.route.params.subscribe(params => {
- this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => {
+ combineLatest([this.signpostingDataService.getLinks(params.id).pipe(take(1)), this.getCoarLdnLocalInboxUrls()])
+ .subscribe(([signpostingLinks, coarRestApiUrls]) => {
let links = '';
this.signpostingLinks = signpostingLinks;
@@ -124,6 +134,11 @@ export class ItemPageComponent implements OnInit, OnDestroy {
this.linkHeadService.addTag(tag);
});
+ if (coarRestApiUrls.length > 0) {
+ let inboxLinks = this.initPageInboxLinks(coarRestApiUrls);
+ links = links + (isNotEmpty(links) ? ', ' : '') + inboxLinks;
+ }
+
if (isPlatformServer(this.platformId)) {
this.responseService.setHeader('Link', links);
}
@@ -131,9 +146,49 @@ export class ItemPageComponent implements OnInit, OnDestroy {
});
}
+ /**
+ * Sets the COAR LDN local inbox URL if COAR configuration is enabled.
+ * If the COAR LDN local inbox URL is retrieved successfully, initializes the page inbox links.
+ */
+ private getCoarLdnLocalInboxUrls(): Observable {
+ return this.notifyInfoService.isCoarConfigEnabled().pipe(
+ switchMap((coarLdnEnabled: boolean) => {
+ if (coarLdnEnabled) {
+ return this.notifyInfoService.getCoarLdnLocalInboxUrls();
+ }
+ })
+ );
+ }
+
+ /**
+ * Initializes the page inbox links.
+ * @param coarRestApiUrls - An array of COAR REST API URLs.
+ */
+ private initPageInboxLinks(coarRestApiUrls: string[]): string {
+ const rel = this.notifyInfoService.getInboxRelationLink();
+ let links = '';
+
+ coarRestApiUrls.forEach((coarRestApiUrl: string) => {
+ // Add link to head
+ let tag: LinkDefinition = {
+ href: coarRestApiUrl,
+ rel: rel
+ };
+ this.inboxTags.push(tag);
+ this.linkHeadService.addTag(tag);
+
+ links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`;
+ });
+
+ return links;
+ }
+
ngOnDestroy(): void {
this.signpostingLinks.forEach((link: SignpostingLink) => {
this.linkHeadService.removeTag(`href='${link.href}'`);
});
+ this.inboxTags.forEach((link: LinkDefinition) => {
+ this.linkHeadService.removeTag(`href='${link.href}'`);
+ });
}
}
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html
index 3749f63964..3014fcb302 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/item-page/simple/item-types/publication/publication.component.html
@@ -84,6 +84,18 @@
[label]="'item.page.uri'">
+
+
+
+
+
+
{{"item.page.link.full" | translate}}
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html
new file mode 100644
index 0000000000..7f9e7fbd4e
--- /dev/null
+++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.html
@@ -0,0 +1,14 @@
+ 0">
+
+ 0">
+
[0]}}-logo.png)
+
+
{{'item.qa-event-notification.check.notification-info' | translate : {num:
+ source.totalEvents } }}
+
+
+
+
+
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss
new file mode 100644
index 0000000000..ab33b46fca
--- /dev/null
+++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.scss
@@ -0,0 +1,8 @@
+
+.source-logo {
+ max-height: var(--ds-header-logo-height);
+}
+
+.sections-gap {
+ gap: 1rem;
+}
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts
new file mode 100644
index 0000000000..4c8a43a1ab
--- /dev/null
+++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts
@@ -0,0 +1,58 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { QaEventNotificationComponent } from './qa-event-notification.component';
+import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
+import { createPaginatedList } from '../../../shared/testing/utils.test';
+import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { TranslateModule } from '@ngx-translate/core';
+import { CommonModule } from '@angular/common';
+import { SplitPipe } from '../../../shared/utils/split.pipe';
+import { RequestService } from '../../../core/data/request.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { provideMockStore } from '@ngrx/store/testing';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
+
+describe('QaEventNotificationComponent', () => {
+ let component: QaEventNotificationComponent;
+ let fixture: ComponentFixture;
+
+ let qualityAssuranceSourceDataServiceStub: any;
+
+ const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()]));
+ const item = Object.assign({ uuid: '1234' });
+
+ beforeEach(async () => {
+ qualityAssuranceSourceDataServiceStub = {
+ getSourcesByTarget: () => obj
+ };
+ await TestBed.configureTestingModule({
+ imports: [CommonModule, TranslateModule.forRoot()],
+ declarations: [QaEventNotificationComponent, SplitPipe],
+ providers: [
+ { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub },
+ { provide: RequestService, useValue: {} },
+ { provide: NotificationsService, useValue: {} },
+ { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test')},
+ ObjectCacheService,
+ RemoteDataBuildService,
+ provideMockStore({})
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(QaEventNotificationComponent);
+ component = fixture.componentInstance;
+ component.item = item;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts
new file mode 100644
index 0000000000..30393367a3
--- /dev/null
+++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts
@@ -0,0 +1,53 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { Item } from '../../../core/shared/item.model';
+import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators';
+import { Observable } from 'rxjs';
+import { AlertType } from '../../../shared/alert/aletr-type';
+import { FindListOptions } from '../../../core/data/find-list-options.model';
+import { RequestParam } from '../../../core/cache/models/request-param.model';
+import { QualityAssuranceSourceDataService } from '../../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
+import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model';
+
+@Component({
+ selector: 'ds-qa-event-notification',
+ templateUrl: './qa-event-notification.component.html',
+ styleUrls: ['./qa-event-notification.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [QualityAssuranceSourceDataService]
+})
+/**
+ * Component for displaying quality assurance event notifications for an item.
+ */
+export class QaEventNotificationComponent {
+
+ /**
+ * The item to display quality assurance event notifications for.
+ */
+ @Input() item: Item;
+
+ /**
+ * The type of alert to display for the notification.
+ */
+ AlertTypeInfo = AlertType.Info;
+
+ constructor(
+ private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService,
+ ) { }
+
+ /**
+ * Returns an Observable of QualityAssuranceSourceObject[] for the current item.
+ * @returns An Observable of QualityAssuranceSourceObject[] for the current item.
+ * Note: sourceId is composed as: id: "sourceName:"
+ */
+ getQualityAssuranceSources$(): Observable {
+ const findListTopicOptions: FindListOptions = {
+ searchParams: [new RequestParam('target', this.item.uuid)]
+ };
+ return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions)
+ .pipe(
+ getFirstCompletedRemoteData(),
+ getRemoteDataPayload(),
+ getPaginatedListPayload(),
+ );
+ }
+}
diff --git a/src/app/my-dspace-page/my-dspace-page.component.html b/src/app/my-dspace-page/my-dspace-page.component.html
index c5e49b0cec..70bcf1b7bc 100644
--- a/src/app/my-dspace-page/my-dspace-page.component.html
+++ b/src/app/my-dspace-page/my-dspace-page.component.html
@@ -1,4 +1,5 @@
+
diff --git a/src/app/my-dspace-page/my-dspace-page.module.ts b/src/app/my-dspace-page/my-dspace-page.module.ts
index b75806cec7..60726bacc4 100644
--- a/src/app/my-dspace-page/my-dspace-page.module.ts
+++ b/src/app/my-dspace-page/my-dspace-page.module.ts
@@ -16,6 +16,7 @@ import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module';
import { UploadModule } from '../shared/upload/upload.module';
import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.module';
+import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
const DECLARATIONS = [
MyDSpacePageComponent,
@@ -23,7 +24,8 @@ const DECLARATIONS = [
MyDSpaceNewSubmissionComponent,
CollectionSelectorComponent,
MyDSpaceNewSubmissionDropdownComponent,
- MyDSpaceNewExternalDropdownComponent
+ MyDSpaceNewExternalDropdownComponent,
+ MyDspaceQaEventsNotificationsComponent,
];
@NgModule({
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html
new file mode 100644
index 0000000000..baf90fdf53
--- /dev/null
+++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.html
@@ -0,0 +1,28 @@
+ 0">
+
+ 0"
+ >
+

+
+
+ {{
+ "mydspace.qa-event-notification.check.notification-info"
+ | translate : { num: source.totalEvents }
+ }}
+
+
+
+
+
+
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss
new file mode 100644
index 0000000000..ab33b46fca
--- /dev/null
+++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.scss
@@ -0,0 +1,8 @@
+
+.source-logo {
+ max-height: var(--ds-header-logo-height);
+}
+
+.sections-gap {
+ gap: 1rem;
+}
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts
new file mode 100644
index 0000000000..4bf42c1319
--- /dev/null
+++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.spec.ts
@@ -0,0 +1,36 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications.component';
+import { QualityAssuranceSourceDataService } from '../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
+import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils';
+import { createPaginatedList } from 'src/app/shared/testing/utils.test';
+import { QualityAssuranceSourceObject } from 'src/app/core/suggestion-notifications/qa/models/quality-assurance-source.model';
+
+describe('MyDspaceQaEventsNotificationsComponent', () => {
+ let component: MyDspaceQaEventsNotificationsComponent;
+ let fixture: ComponentFixture;
+
+ let qualityAssuranceSourceDataServiceStub: any;
+ const obj = createSuccessfulRemoteDataObject$(createPaginatedList([new QualityAssuranceSourceObject()]));
+
+ beforeEach(async () => {
+ qualityAssuranceSourceDataServiceStub = {
+ getSources: () => obj
+ };
+ await TestBed.configureTestingModule({
+ declarations: [ MyDspaceQaEventsNotificationsComponent ],
+ providers: [
+ { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }
+ ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(MyDspaceQaEventsNotificationsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts
new file mode 100644
index 0000000000..9992ec9ff8
--- /dev/null
+++ b/src/app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component.ts
@@ -0,0 +1,44 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { QualityAssuranceSourceDataService } from '../../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
+import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../core/shared/operators';
+import { Observable, of, tap } from 'rxjs';
+import { QualityAssuranceSourceObject } from 'src/app/core/suggestion-notifications/qa/models/quality-assurance-source.model';
+
+@Component({
+ selector: 'ds-my-dspace-qa-events-notifications',
+ templateUrl: './my-dspace-qa-events-notifications.component.html',
+ styleUrls: ['./my-dspace-qa-events-notifications.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class MyDspaceQaEventsNotificationsComponent implements OnInit {
+
+ /**
+ * An Observable that emits an array of QualityAssuranceSourceObject.
+ */
+ sources$: Observable = of([]);
+
+ constructor(private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService) { }
+
+ ngOnInit(): void {
+ this.getSources();
+ }
+
+ /**
+ * Retrieves the sources for Quality Assurance.
+ * @returns An Observable of the sources for Quality Assurance.
+ * @throws An error if the retrieval of Quality Assurance sources fails.
+ */
+ getSources() {
+ this.sources$ = this.qualityAssuranceSourceDataService.getSources()
+ .pipe(
+ getFirstCompletedRemoteData(),
+ tap((rd) => {
+ if (rd.hasFailed) {
+ throw new Error('Can\'t retrieve Quality Assurance sources');
+ }
+ }),
+ getRemoteDataPayload(),
+ getPaginatedListPayload(),
+ );
+ }
+}
diff --git a/src/app/shared/mocks/notifications.mock.ts b/src/app/shared/mocks/notifications.mock.ts
index dc1c98c7b9..82ba818b13 100644
--- a/src/app/shared/mocks/notifications.mock.ts
+++ b/src/app/shared/mocks/notifications.mock.ts
@@ -1838,8 +1838,8 @@ export function getMockNotificationsStateService(): any {
*/
export function getMockQualityAssuranceTopicRestService(): QualityAssuranceTopicDataService {
return jasmine.createSpyObj('QualityAssuranceTopicDataService', {
- getTopics: jasmine.createSpy('getTopics'),
- getTopic: jasmine.createSpy('getTopic'),
+ searchTopicsBySource: jasmine.createSpy('searchTopicsBySource'),
+ searchTopicsByTarget: jasmine.createSpy('searchTopicsByTarget'),
});
}
diff --git a/src/app/shared/mocks/suggestion.mock.ts b/src/app/shared/mocks/suggestion.mock.ts
index fd1a7c41f1..8b1ab7acd5 100644
--- a/src/app/shared/mocks/suggestion.mock.ts
+++ b/src/app/shared/mocks/suggestion.mock.ts
@@ -1,7 +1,6 @@
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Item } from '../../core/shared/item.model';
import { SearchResult } from '../search/models/search-result.model';
-import { SuggestionsService } from '../../suggestion-notifications/reciter-suggestions/suggestions.service';
// REST Mock ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 0f7871f7f9..6c9cf1c3b3 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -284,6 +284,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
+import { SplitPipe } from './utils/split.pipe';
const MODULES = [
CommonModule,
@@ -323,7 +324,8 @@ const PIPES = [
ObjNgFor,
BrowserOnlyPipe,
MarkdownPipe,
- ShortNumberPipe
+ ShortNumberPipe,
+ SplitPipe,
];
const COMPONENTS = [
diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts
new file mode 100644
index 0000000000..4da1b1323e
--- /dev/null
+++ b/src/app/shared/utils/split.pipe.ts
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'dsSplit'
+})
+export class SplitPipe implements PipeTransform {
+
+ transform(value: string, separator: string): string[] {
+ return value.split(separator);
+ }
+
+}
diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html
index 4a916cfe23..b3536a6fbf 100644
--- a/src/app/submission/form/submission-form.component.html
+++ b/src/app/submission/form/submission-form.component.html
@@ -9,7 +9,8 @@
+
diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html
index e6ae9d1b9c..99bcec168f 100644
--- a/src/app/submission/sections/container/section-container.component.html
+++ b/src/app/submission/sections/container/section-container.component.html
@@ -48,4 +48,4 @@
-
\ No newline at end of file
+
diff --git a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts
new file mode 100644
index 0000000000..7b8d309667
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts
@@ -0,0 +1,122 @@
+import { Injectable } from '@angular/core';
+import { dataService } from '../../../core/data/base/data-service.decorator';
+import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
+import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data';
+import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data';
+import { RequestService } from '../../../core/data/request.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { FindListOptions } from '../../../core/data/find-list-options.model';
+import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
+import { Observable } from 'rxjs';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list.model';
+import { NoContent } from '../../../core/shared/NoContent.model';
+import { map, take } from 'rxjs/operators';
+import { URLCombiner } from '../../../core/url-combiner/url-combiner';
+import { MultipartPostRequest } from '../../../core/data/request.models';
+import { RestRequest } from '../../../core/data/rest-request.model';
+import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type';
+import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config';
+import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data';
+import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data';
+import { ChangeAnalyzer } from '../../../core/data/change-analyzer';
+import { Operation } from 'fast-json-patch';
+import { RestRequestMethod } from '../../../core/data/rest-request-method';
+import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
+import { hasValue } from '../../../shared/empty.util';
+
+
+/**
+ * A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint
+ */
+@Injectable()
+@dataService(SUBMISSION_COAR_NOTIFY_CONFIG)
+export class CoarNotifyConfigDataService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData {
+ createData: CreateDataImpl;
+ private findAllData: FindAllDataImpl;
+ private deleteData: DeleteDataImpl;
+ private patchData: PatchDataImpl;
+ private comparator: ChangeAnalyzer;
+
+ constructor(
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService,
+ protected notificationsService: NotificationsService,
+ ) {
+ super('submissioncoarnotifyconfigs', requestService, rdbService, objectCache, halService);
+
+ this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
+ this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
+ this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint);
+ this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
+ }
+
+
+ create(object: SubmissionCoarNotifyConfig): Observable> {
+ return this.createData.create(object);
+ }
+
+ patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable> {
+ return this.patchData.patch(object, operations);
+ }
+
+ update(object: SubmissionCoarNotifyConfig): Observable> {
+ return this.patchData.update(object);
+ }
+
+ commitUpdates(method?: RestRequestMethod): void {
+ return this.patchData.commitUpdates(method);
+ }
+
+ createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable {
+ return this.patchData.createPatchFromCache(object);
+ }
+
+ findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
+ public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> {
+ return this.deleteData.delete(objectId, copyVirtualMetadata);
+ }
+
+ public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> {
+ return this.deleteData.deleteByHref(href, copyVirtualMetadata);
+ }
+
+ public invoke(serviceName: string, serviceId: string, files: File[]): Observable> {
+ const requestId = this.requestService.generateRequestId();
+ this.getBrowseEndpoint().pipe(
+ take(1),
+ map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'submissioncoarnotifyconfigmodel', serviceId).toString()),
+ map((endpoint: string) => {
+ const body = this.getInvocationFormData(files);
+ return new MultipartPostRequest(requestId, endpoint, body);
+ })
+ ).subscribe((request: RestRequest) => this.requestService.send(request));
+
+ return this.rdbService.buildFromRequestUUID(requestId);
+ }
+
+ public SubmissionCoarNotifyConfigModelWithNameExistsAndCanExecute(scriptName: string): Observable {
+ return this.findById(scriptName).pipe(
+ getFirstCompletedRemoteData(),
+ map((rd: RemoteData) => {
+ return hasValue(rd.payload);
+ }),
+ );
+ }
+
+ private getInvocationFormData(files: File[]): FormData {
+ const form: FormData = new FormData();
+ files.forEach((file: File) => {
+ form.append('file', file);
+ });
+ return form;
+ }
+}
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts
new file mode 100644
index 0000000000..53e41783ce
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts
@@ -0,0 +1,13 @@
+/**
+ * The resource type for Ldn-Services
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+import { ResourceType } from '../../../core/shared/resource-type';
+
+
+export const SUBMISSION_COAR_NOTIFY_CONFIG = new ResourceType('submissioncoarnotifyconfig');
+
+export const COAR_NOTIFY_WORKSPACEITEM = new ResourceType('workspaceitem');
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html
new file mode 100644
index 0000000000..e3bdffd065
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html
@@ -0,0 +1,124 @@
+
+
0">
+
+
+
+
+
+
+
+ 0}"
+ class="form-control w-100 scrollable-dropdown-input"
+ [value]="ldnServiceByPattern[pattern][serviceIndex]?.name"
+ (click)="myDropdown.open()"
+ />
+
+
+
+
+
+ {{'submission.section.section-coar-notify.small.notification' | translate : {pattern : pattern} }}
+
+
0">
+
+ {{ error.message | translate}}
+
+
+
+
+
+
+
{{ 'submission.section.section-coar-notify.selection.description' | translate }}
+
+ {{ ldnServiceByPattern[pattern][serviceIndex].description }}
+
+
+
+ {{ 'submission.section.section-coar-notify.selection.no-description' | translate }}
+
+
+
+
+
+
0">
+
+
+
+ {{ 'submission.section.section-coar-notify.notification.error' | translate }}
+
+
+
+
+
+
+
+
+
+ {{'submission.section.section-coar-notify.info.no-pattern' | translate }}
+
+
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss
new file mode 100644
index 0000000000..3ac8827f74
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss
@@ -0,0 +1,4 @@
+// Getting styles for NgbDropdown
+@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss';
+@import '../../../shared/form/form.component.scss';
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts
new file mode 100644
index 0000000000..a66e24237f
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts
@@ -0,0 +1,406 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify.component';
+import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
+import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
+import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
+import { SectionsService } from '../sections.service';
+import { CoarNotifyConfigDataService } from './coar-notify-config-data.service';
+import { ChangeDetectorRef } from '@angular/core';
+import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { createPaginatedList } from '../../../shared/testing/utils.test';
+import { of } from 'rxjs';
+import { LdnService } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
+import { NotifyServicePattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model';
+
+describe('SubmissionSectionCoarNotifyComponent', () => {
+ let component: SubmissionSectionCoarNotifyComponent;
+ let componentAsAny: any;
+ let fixture: ComponentFixture;
+
+ let ldnServicesService: jasmine.SpyObj;
+ let coarNotifyConfigDataService: jasmine.SpyObj;
+ let operationsBuilder: jasmine.SpyObj;
+ let sectionService: jasmine.SpyObj;
+ let cdRefStub: any;
+
+ const patterns: SubmissionCoarNotifyConfig[] = Object.assign(
+ [new SubmissionCoarNotifyConfig()],
+ {
+ patterns: ['review', 'endorsment'],
+ }
+ );
+ const patternsPL = createPaginatedList(patterns);
+ const coarNotifyConfig = createSuccessfulRemoteDataObject$(patternsPL);
+
+ beforeEach(async () => {
+ ldnServicesService = jasmine.createSpyObj('LdnServicesService', [
+ 'findByInboundPattern',
+ ]);
+ coarNotifyConfigDataService = jasmine.createSpyObj(
+ 'CoarNotifyConfigDataService',
+ ['findAll']
+ );
+ operationsBuilder = jasmine.createSpyObj('JsonPatchOperationsBuilder', [
+ 'remove',
+ 'replace',
+ 'add',
+ ]);
+ sectionService = jasmine.createSpyObj('SectionsService', [
+ 'dispatchRemoveSectionErrors',
+ 'getSectionServerErrors',
+ 'setSectionError',
+ ]);
+ cdRefStub = Object.assign({
+ detectChanges: () => fixture.detectChanges(),
+ });
+
+ await TestBed.configureTestingModule({
+ declarations: [SubmissionSectionCoarNotifyComponent],
+ providers: [
+ { provide: LdnServicesService, useValue: ldnServicesService },
+ { provide: CoarNotifyConfigDataService, useValue: coarNotifyConfigDataService},
+ { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder },
+ { provide: SectionsService, useValue: sectionService },
+ { provide: ChangeDetectorRef, useValue: cdRefStub },
+ { provide: 'collectionIdProvider', useValue: 'collectionId' },
+ { provide: 'sectionDataProvider', useValue: { id: 'sectionId', data: {} }},
+ { provide: 'submissionIdProvider', useValue: 'submissionId' },
+ NgbDropdown,
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SubmissionSectionCoarNotifyComponent);
+ component = fixture.componentInstance;
+ componentAsAny = component;
+
+ component.patterns = patterns[0].patterns;
+ coarNotifyConfigDataService.findAll.and.returnValue(coarNotifyConfig);
+ sectionService.getSectionServerErrors.and.returnValue(
+ of(
+ Object.assign([], {
+ path: 'sections/sectionId/data/notifyCoar',
+ message: 'error',
+ })
+ )
+ );
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('onSectionInit', () => {
+ it('should call setCoarNotifyConfig and getSectionServerErrorsAndSetErrorsToDisplay', () => {
+ spyOn(component, 'setCoarNotifyConfig');
+ spyOn(componentAsAny, 'getSectionServerErrorsAndSetErrorsToDisplay');
+
+ component.onSectionInit();
+
+ expect(component.setCoarNotifyConfig).toHaveBeenCalled();
+ expect(componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay).toHaveBeenCalled();
+ });
+ });
+
+ describe('onChange', () => {
+ const pattern = 'review';
+ const index = 0;
+ const selectedService: LdnService = Object.assign(new LdnService(), {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [
+ {
+ pattern: 'review',
+ },
+ ],
+ description: '',
+ });
+
+ beforeEach(() => {
+ component.ldnServiceByPattern[pattern] = [];
+ });
+
+ it('should do nothing if the selected value is the same as the previous one', () => {
+ component.ldnServiceByPattern[pattern][index] = selectedService;
+ component.onChange(pattern, index, selectedService);
+
+ expect(componentAsAny.operationsBuilder.remove).not.toHaveBeenCalled();
+ expect(componentAsAny.operationsBuilder.replace).not.toHaveBeenCalled();
+ expect(componentAsAny.operationsBuilder.add).not.toHaveBeenCalled();
+ });
+
+ it('should remove the path when the selected value is null', () => {
+ component.ldnServiceByPattern[pattern][index] = selectedService;
+ component.onChange(pattern, index, null);
+
+ expect(componentAsAny.operationsBuilder.remove).toHaveBeenCalledWith(
+ componentAsAny.pathCombiner.getPath([pattern, index.toString()])
+ );
+ expect(component.ldnServiceByPattern[pattern][index]).toBeNull();
+ expect(component.previousServices[pattern][index]).toBeNull();
+ });
+
+ it('should replace the path when there is a previous value stored and it is different from the new one', () => {
+ const previousService: LdnService = Object.assign(new LdnService(), {
+ id: 2,
+ name: 'service2',
+ notifyServiceInboundPatterns: [
+ {
+ pattern: 'endorsement',
+ },
+ ],
+ description: 'test',
+ });
+ component.ldnServiceByPattern[pattern][index] = previousService;
+ component.previousServices[pattern] = [];
+ component.previousServices[pattern][index] = previousService.id;
+ component.onChange(pattern, index, selectedService);
+
+ expect(componentAsAny.operationsBuilder.replace).toHaveBeenCalledWith(
+ componentAsAny.pathCombiner.getPath([pattern, index.toString()]),
+ selectedService.id,
+ true
+ );
+ expect(component.ldnServiceByPattern[pattern][index]).toEqual(
+ selectedService
+ );
+ expect(component.previousServices[pattern][index]).toEqual(
+ selectedService.id
+ );
+ });
+
+ it('should add the path when there is no previous value stored', () => {
+ component.onChange(pattern, index, selectedService);
+
+ expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith(
+ componentAsAny.pathCombiner.getPath([pattern, '-']),
+ [selectedService.id],
+ false,
+ true
+ );
+ expect(component.ldnServiceByPattern[pattern][index]).toEqual(
+ selectedService
+ );
+ expect(component.previousServices[pattern][index]).toEqual(
+ selectedService.id
+ );
+ });
+ });
+
+ describe('initSelectedServicesByPattern', () => {
+ const pattern1 = 'review';
+ const pattern2 = 'endorsement';
+ const service1: LdnService = Object.assign(new LdnService(), {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern1,
+ }),
+ ],
+ });
+ const service2: LdnService = Object.assign(new LdnService(), {
+ id: 2,
+ name: 'service2',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern2,
+ }),
+ ],
+ });
+ const service3: LdnService = Object.assign(new LdnService(), {
+ id: 3,
+ name: 'service3',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern1,
+ }),
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern2,
+ }),
+ ],
+ });
+
+ const services = [service1, service2, service3];
+
+ beforeEach(() => {
+ spyOn(component, 'filterServices').and.callFake((pattern) => {
+ return of(
+ services.filter((service) =>
+ component.hasInboundPattern(service, pattern)
+ )
+ );
+ });
+ });
+
+ it('should initialize the selected services by pattern', () => {
+ component.patterns = [pattern1, pattern2];
+ component.initSelectedServicesByPattern();
+
+ expect(component.ldnServiceByPattern[pattern1]).toEqual([null]);
+ expect(component.ldnServiceByPattern[pattern2]).toEqual([null]);
+ });
+
+ it('should add the service to the selected services by pattern if the section data has a value for the pattern', () => {
+ component.patterns = [pattern1, pattern2];
+ component.sectionData.data[pattern1] = [service1.id, service3.id];
+ component.sectionData.data[pattern2] = [service2.id, service3.id];
+ component.initSelectedServicesByPattern();
+
+ expect(component.ldnServiceByPattern[pattern1]).toEqual([
+ service1,
+ service3,
+ ]);
+ expect(component.ldnServiceByPattern[pattern2]).toEqual([
+ service2,
+ service3,
+ ]);
+ });
+ });
+
+ describe('addService', () => {
+ const pattern = 'review';
+ const service: any = {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [{ pattern: pattern }],
+ };
+
+ beforeEach(() => {
+ component.ldnServiceByPattern[pattern] = [];
+ });
+
+ it('should push the new service to the array corresponding to the pattern', () => {
+ component.addService(pattern, service);
+
+ expect(component.ldnServiceByPattern[pattern]).toEqual([service]);
+ });
+ });
+
+ describe('removeService', () => {
+ const pattern = 'review';
+ const service1: LdnService = Object.assign(new LdnService(), {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern,
+ }),
+ ],
+ });
+ const service2: LdnService = Object.assign(new LdnService(), {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern,
+ }),
+ ],
+ });
+ const service3: LdnService = Object.assign(new LdnService(), {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [
+ Object.assign(new NotifyServicePattern(), {
+ pattern: pattern,
+ }),
+ ],
+ });
+
+ beforeEach(() => {
+ component.ldnServiceByPattern[pattern] = [service1, service2, service3];
+ });
+
+ it('should remove the service at the specified index from the array corresponding to the pattern', () => {
+ component.removeService(pattern, 1);
+
+ expect(component.ldnServiceByPattern[pattern]).toEqual([
+ service1,
+ service3,
+ ]);
+ });
+ });
+
+ describe('filterServices', () => {
+ const pattern = 'review';
+ const service1: any = {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [{ pattern: pattern }],
+ };
+ const service2: any = {
+ id: 2,
+ name: 'service2',
+ notifyServiceInboundPatterns: [{ pattern: pattern }],
+ };
+ const service3: any = {
+ id: 3,
+ name: 'service3',
+ notifyServiceInboundPatterns: [{ pattern: pattern }],
+ };
+ const services = [service1, service2, service3];
+
+ beforeEach(() => {
+ ldnServicesService.findByInboundPattern.and.returnValue(
+ createSuccessfulRemoteDataObject$(createPaginatedList(services))
+ );
+ });
+
+ it('should return an observable of the services that match the given pattern', () => {
+ component.filterServices(pattern).subscribe((result) => {
+ expect(result).toEqual(services);
+ });
+ });
+ });
+
+ describe('hasInboundPattern', () => {
+ const pattern = 'review';
+ const service: any = {
+ id: 1,
+ name: 'service1',
+ notifyServiceInboundPatterns: [{ pattern: pattern }],
+ };
+
+ it('should return true if the service has the specified inbound pattern type', () => {
+ expect(component.hasInboundPattern(service, pattern)).toBeTrue();
+ });
+
+ it('should return false if the service does not have the specified inbound pattern type', () => {
+ expect(component.hasInboundPattern(service, 'endorsement')).toBeFalse();
+ });
+ });
+
+ describe('getSectionServerErrorsAndSetErrorsToDisplay', () => {
+ it('should set the validation errors for the current section to display', () => {
+ const validationErrors = [
+ { path: 'sections/sectionId/data/notifyCoar', message: 'error' },
+ ];
+ sectionService.getSectionServerErrors.and.returnValue(
+ of(validationErrors)
+ );
+
+ componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay();
+
+ expect(sectionService.setSectionError).toHaveBeenCalledWith(
+ component.submissionId,
+ component.sectionData.id,
+ validationErrors[0]
+ );
+ });
+ });
+
+ describe('onSectionDestroy', () => {
+ it('should unsubscribe from all subscriptions', () => {
+ const sub1 = of(null).subscribe();
+ const sub2 = of(null).subscribe();
+ componentAsAny.subs = [sub1, sub2];
+ spyOn(sub1, 'unsubscribe');
+ spyOn(sub2, 'unsubscribe');
+ component.onSectionDestroy();
+ expect(sub1.unsubscribe).toHaveBeenCalled();
+ expect(sub2.unsubscribe).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts
new file mode 100644
index 0000000000..92f56b4f83
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts
@@ -0,0 +1,293 @@
+import { ChangeDetectorRef, Component, Inject } from '@angular/core';
+import { Observable, Subscription } from 'rxjs';
+import { SectionModelComponent } from '../models/section.model';
+import { renderSectionFor } from '../sections-decorator';
+import { SectionsType } from '../sections-type';
+import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
+import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
+import { SectionsService } from '../sections.service';
+import { SectionDataObject } from '../models/section-data.model';
+
+import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
+
+import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators';
+import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
+import { LdnService } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model';
+import { CoarNotifyConfigDataService } from './coar-notify-config-data.service';
+import { filter, map, take, tap } from 'rxjs/operators';
+import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
+import { SubmissionSectionError } from '../../objects/submission-section-error.model';
+
+/**
+ * This component represents a section that contains the submission section-coar-notify form.
+ */
+@Component({
+ selector: 'ds-submission-section-coar-notify',
+ templateUrl: './section-coar-notify.component.html',
+ styleUrls: ['./section-coar-notify.component.scss'],
+ providers: [NgbDropdown]
+})
+@renderSectionFor(SectionsType.CoarNotify)
+export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent {
+
+ /**
+ * Contains an array of string patterns.
+ */
+ patterns: string[] = [];
+ /**
+ * An object that maps string keys to arrays of LdnService objects.
+ * Used to store LdnService objects by pattern.
+ */
+ ldnServiceByPattern: { [key: string]: LdnService[] } = {};
+ /**
+ * A map representing all services for each pattern
+ * {
+ * 'pattern': {
+ * 'index': 'service.id'
+ * }
+ * }
+ *
+ * @type {{ [key: string]: {[key: number]: number} }}
+ * @memberof SubmissionSectionCoarNotifyComponent
+ */
+ previousServices: { [key: string]: {[key: number]: number} } = {};
+
+ /**
+ * The [[JsonPatchOperationPathCombiner]] object
+ * @type {JsonPatchOperationPathCombiner}
+ */
+ protected pathCombiner: JsonPatchOperationPathCombiner;
+ /**
+ * A map representing all field on their way to be removed
+ * @type {Map}
+ */
+ protected fieldsOnTheirWayToBeRemoved: Map = new Map();
+ /**
+ * Array to track all subscriptions and unsubscribe them onDestroy
+ * @type {Array}
+ */
+ protected subs: Subscription[] = [];
+
+ constructor(protected ldnServicesService: LdnServicesService,
+ // protected formOperationsService: SectionFormOperationsService,
+ protected operationsBuilder: JsonPatchOperationsBuilder,
+ protected sectionService: SectionsService,
+ protected coarNotifyConfigDataService: CoarNotifyConfigDataService,
+ protected chd: ChangeDetectorRef,
+ @Inject('collectionIdProvider') public injectedCollectionId: string,
+ @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
+ @Inject('submissionIdProvider') public injectedSubmissionId: string) {
+ super(injectedCollectionId, injectedSectionData, injectedSubmissionId);
+ }
+
+ /**
+ * Initialize all instance variables
+ */
+ onSectionInit() {
+ this.setCoarNotifyConfig();
+ this.getSectionServerErrorsAndSetErrorsToDisplay();
+ this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id);
+ }
+
+ /**
+ * Method called when section is initialized
+ * Retriev available NotifyConfigs
+ */
+ setCoarNotifyConfig() {
+ this.subs.push(
+ this.coarNotifyConfigDataService.findAll().pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((data) => {
+ if (data.hasSucceeded) {
+ this.patterns = data.payload.page[0].patterns;
+ this.initSelectedServicesByPattern();
+ }
+ }));
+ }
+
+ /**
+ * Handles the change event of a select element.
+ * @param pattern - The pattern of the select element.
+ * @param index - The index of the select element.
+ */
+ onChange(pattern: string, index: number, selectedService: LdnService | null) {
+ // do nothing if the selected value is the same as the previous one
+ if (this.ldnServiceByPattern[pattern][index]?.id === selectedService?.id) {
+ return;
+ }
+
+ // initialize the previousServices object for the pattern if it does not exist
+ if (!this.previousServices[pattern]) {
+ this.previousServices[pattern] = {};
+ }
+
+ if (hasNoValue(selectedService)) {
+ // on value change, remove the path when the selected value is null
+ // and remove the previous value stored for the same index and pattern
+ this.operationsBuilder.remove(this.pathCombiner.getPath([pattern, index.toString()]));
+ this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id);
+ this.ldnServiceByPattern[pattern][index] = null;
+ this.previousServices[pattern][index] = null;
+ return;
+ }
+ // store the previous value
+ this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index]?.id;
+ // set the new value
+ this.ldnServiceByPattern[pattern][index] = selectedService;
+
+ const hasPrevValueStored = hasValue(this.previousServices[pattern][index]) && this.previousServices[pattern][index] !== selectedService.id;
+ if (hasPrevValueStored) {
+ // replace the path
+ // when there is a previous value stored and it is different from the new one
+ this.operationsBuilder.replace(this.pathCombiner.getPath([pattern, index.toString()]), selectedService.id, true);
+ } else {
+ // add the path when there is no previous value stored
+ this.operationsBuilder.add(this.pathCombiner.getPath([pattern, '-']), [selectedService.id], false, true);
+ }
+ // set the previous value to the new value
+ this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index].id;
+ this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id);
+ this.chd.detectChanges();
+ }
+
+ /**
+ * Initializes the selected services by pattern.
+ * Loops through each pattern and filters the services based on the pattern.
+ * If the section data has a value for the pattern, it adds the service to the selected services by pattern.
+ * If the section data does not have a value for the pattern, it adds a null service to the selected services by pattern,
+ * so that the select element is initialized with a null value and to display the default select input.
+ */
+ initSelectedServicesByPattern(): void {
+ this.patterns.forEach((pattern) => {
+ if (hasValue(this.sectionData.data[pattern])) {
+ this.subs.push(
+ this.filterServices(pattern)
+ .subscribe((services: LdnService[]) => {
+ const selectedServices = services.filter((service) => {
+ const selection = (this.sectionData.data[pattern] as LdnService[]).find((s: LdnService) => s.id === service.id);
+ this.addService(pattern, selection);
+ return this.sectionData.data[pattern].includes(service.id);
+ });
+ this.ldnServiceByPattern[pattern] = selectedServices;
+ })
+ );
+ } else {
+ this.ldnServiceByPattern[pattern] = [];
+ this.addService(pattern, null);
+ }
+ });
+ }
+
+ /**
+ * Adds a new service to the selected services for the given pattern.
+ * @param pattern - The pattern to add the new service to.
+ * @param newService - The new service to add.
+ */
+ addService(pattern: string, newService: LdnService) {
+ // Your logic to add a new service to the selected services for the pattern
+ // Example: Push the newService to the array corresponding to the pattern
+ if (!this.ldnServiceByPattern[pattern]) {
+ this.ldnServiceByPattern[pattern] = [];
+ }
+ this.ldnServiceByPattern[pattern].push(newService);
+ }
+
+ /**
+ * Removes the service at the specified index from the array corresponding to the pattern.
+ * (part of next phase of implementation)
+ */
+ removeService(pattern: string, serviceIndex: number) {
+ if (this.ldnServiceByPattern[pattern]) {
+ // Remove the service at the specified index from the array
+ this.ldnServiceByPattern[pattern].splice(serviceIndex, 1);
+ }
+ }
+
+ /**
+ * Method called when dropdowns for the section are initialized
+ * Retrieve services with corresponding patterns to the dropdowns.
+ */
+ filterServices(pattern: string): Observable {
+ return this.ldnServicesService.findByInboundPattern(pattern).pipe(
+ getFirstCompletedRemoteData(),
+ tap((rd) => {
+ if (rd.hasFailed) {
+ throw new Error(`Failed to retrieve services for pattern ${pattern}`);
+ }
+ }),
+ filter((rd) => rd.hasSucceeded),
+ getRemoteDataPayload(),
+ getPaginatedListPayload(),
+ map((res: LdnService[]) => res.filter((service) =>
+ this.hasInboundPattern(service, pattern)))
+ );
+ }
+
+ /**
+ * Checks if the given service has the specified inbound pattern type.
+ * @param service - The service to check.
+ * @param patternType - The inbound pattern type to look for.
+ * @returns True if the service has the specified inbound pattern type, false otherwise.
+ */
+ hasInboundPattern(service: any, patternType: string): boolean {
+ return service.notifyServiceInboundPatterns.some((pattern: { pattern: string }) => {
+ return pattern.pattern === patternType;
+ });
+ }
+
+ /**
+ * Retrieves server errors for the current section and sets them to display.
+ * @returns An Observable that emits the validation errors for the current section.
+ */
+ private getSectionServerErrorsAndSetErrorsToDisplay() {
+ this.subs.push(
+ this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe(
+ take(1),
+ filter((validationErrors) => isNotEmpty(validationErrors)),
+ ).subscribe((validationErrors: SubmissionSectionError[]) => {
+ if (isNotEmpty(validationErrors)) {
+ validationErrors.forEach((error) => {
+ this.sectionService.setSectionError(this.submissionId, this.sectionData.id, error);
+ });
+ }
+ }));
+ }
+
+ /**
+ * Returns an observable of the errors for the current section that match the given pattern and index.
+ * @param pattern - The pattern to match against the error paths.
+ * @param index - The index to match against the error paths.
+ * @returns An observable of the errors for the current section that match the given pattern and index.
+ */
+ public getShownSectionErrors$(pattern: string, index: number): Observable {
+ return this.sectionService.getShownSectionErrors(this.submissionId, this.sectionData.id, this.sectionData.sectionType)
+ .pipe(
+ take(1),
+ filter((validationErrors) => isNotEmpty(validationErrors)),
+ map((validationErrors: SubmissionSectionError[]) => {
+ return validationErrors.filter((error) => {
+ const path = `${pattern}/${index}`;
+ return error.path.includes(path);
+ });
+ })
+ );
+ }
+
+ /**
+ * @returns An observable that emits a boolean indicating whether the section has any server errors or not.
+ */
+ protected getSectionStatus(): Observable {
+ return this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe(
+ map((validationErrors) => isEmpty(validationErrors)
+ ));
+ }
+
+ /**
+ * Unsubscribe from all subscriptions
+ */
+ onSectionDestroy() {
+ this.subs
+ .filter((subscription) => hasValue(subscription))
+ .forEach((subscription) => subscription.unsubscribe());
+ }
+}
diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts
new file mode 100644
index 0000000000..41ef69cd7a
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts
@@ -0,0 +1,35 @@
+import { CacheableObject } from '../../../core/cache/cacheable-object.model';
+import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
+
+import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
+import { typedObject } from '../../../core/cache/builders/build-decorators';
+import { COAR_NOTIFY_WORKSPACEITEM } from './section-coar-notify-service.resource-type';
+
+
+/** An CoarNotify and its properties. */
+@typedObject
+@inheritSerialization(CacheableObject)
+export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject {
+ static type = COAR_NOTIFY_WORKSPACEITEM;
+
+ @excludeFromEquals
+ @autoserialize
+ endorsement?: number[];
+
+ @deserializeAs('id')
+ review?: number[];
+
+ @autoserialize
+ ingest?: number[];
+
+ @deserialize
+ _links: {
+ self: {
+ href: string;
+ };
+ };
+
+ get self(): string {
+ return this._links.self.href;
+ }
+}
diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts
new file mode 100644
index 0000000000..04973f80c8
--- /dev/null
+++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts
@@ -0,0 +1,39 @@
+import { ResourceType } from '../../../core/shared/resource-type';
+import { CacheableObject } from '../../../core/cache/cacheable-object.model';
+import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
+
+import { excludeFromEquals } from '../../../core/utilities/equals.decorators';
+import { typedObject } from '../../../core/cache/builders/build-decorators';
+import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type';
+
+
+/** A SubmissionCoarNotifyConfig and its properties. */
+@typedObject
+@inheritSerialization(CacheableObject)
+export class SubmissionCoarNotifyConfig extends CacheableObject {
+ static type = SUBMISSION_COAR_NOTIFY_CONFIG;
+
+ @excludeFromEquals
+ @autoserialize
+ type: ResourceType;
+
+ @autoserialize
+ id: string;
+
+ @deserializeAs('id')
+ uuid: string;
+
+ @autoserialize
+ patterns: string[];
+
+ @deserialize
+ _links: {
+ self: {
+ href: string;
+ };
+ };
+
+ get self(): string {
+ return this._links.self.href;
+ }
+}
diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts
index 6bca8a7252..5f71d1731d 100644
--- a/src/app/submission/sections/sections-type.ts
+++ b/src/app/submission/sections/sections-type.ts
@@ -9,4 +9,5 @@ export enum SectionsType {
SherpaPolicies = 'sherpaPolicy',
Identifiers = 'identifiers',
Collection = 'collection',
+ CoarNotify = 'coarnotify'
}
diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts
index cf0ab2b369..d839565f8d 100644
--- a/src/app/submission/submission.module.ts
+++ b/src/app/submission/submission.module.ts
@@ -10,7 +10,7 @@ import { SubmissionFormFooterComponent } from './form/footer/submission-form-foo
import { SubmissionFormComponent } from './form/submission-form.component';
import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component';
import { SubmissionSectionContainerComponent } from './sections/container/section-container.component';
-import { CommonModule } from '@angular/common';
+import { CommonModule, NgOptimizedImage } from '@angular/common';
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { submissionReducers, SubmissionState } from './submission.reducers';
@@ -67,6 +67,11 @@ import {
} from './sections/sherpa-policies/metadata-information/metadata-information.component';
import { SectionFormOperationsService } from './sections/form/section-form-operations.service';
import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component';
+import { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component';
+import {
+ CoarNotifyConfigDataService
+} from './sections/section-coar-notify/coar-notify-config-data.service';
+import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -76,6 +81,7 @@ const ENTRY_COMPONENTS = [
SubmissionSectionCcLicensesComponent,
SubmissionSectionAccessesComponent,
SubmissionSectionSherpaPoliciesComponent,
+ SubmissionSectionCoarNotifyComponent
];
const DECLARATIONS = [
@@ -109,20 +115,22 @@ const DECLARATIONS = [
];
@NgModule({
- imports: [
- CommonModule,
- CoreModule.forRoot(),
- SharedModule,
- StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig),
- EffectsModule.forFeature(submissionEffects),
- JournalEntitiesModule.withEntryComponents(),
- ResearchEntitiesModule.withEntryComponents(),
- FormModule,
- NgbModalModule,
- NgbCollapseModule,
- NgbAccordionModule,
- UploadModule,
- ],
+ imports: [
+ CommonModule,
+ CoreModule.forRoot(),
+ SharedModule,
+ StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig),
+ EffectsModule.forFeature(),
+ EffectsModule.forFeature(submissionEffects),
+ JournalEntitiesModule.withEntryComponents(),
+ ResearchEntitiesModule.withEntryComponents(),
+ FormModule,
+ NgbModalModule,
+ NgbCollapseModule,
+ NgbAccordionModule,
+ UploadModule,
+ NgOptimizedImage,
+ ],
declarations: DECLARATIONS,
exports: [
...DECLARATIONS,
@@ -135,6 +143,8 @@ const DECLARATIONS = [
SubmissionAccessesConfigDataService,
SectionAccessesService,
SectionFormOperationsService,
+ CoarNotifyConfigDataService,
+ LdnServicesService
]
})
diff --git a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html
index 7f1b166d24..5b4757ce8b 100644
--- a/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html
+++ b/src/app/suggestion-notifications/qa/events/quality-assurance-events.component.html
@@ -4,13 +4,19 @@
-
+
+
+
+
+
+ {{(getTargetItemTitle() | async)}}
+