From 90a1f25ba9dce010f49c32b6b45ecd2db8fcfadc Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 27 Apr 2023 16:51:41 +0200 Subject: [PATCH 01/15] Work for signposting --- server.ts | 9 +++ .../metadata-item.service.spec.ts | 16 +++++ .../metadata-item/metadata-item.service.ts | 70 +++++++++++++++++++ src/app/core/metadata/metadata.service.ts | 11 ++- src/app/init.service.ts | 3 + src/modules/app/browser-init.service.ts | 3 + src/modules/app/server-init.service.ts | 5 +- 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/app/core/metadata-item/metadata-item.service.spec.ts create mode 100644 src/app/core/metadata-item/metadata-item.service.ts diff --git a/server.ts b/server.ts index 3e10677a8b..5a3660e4de 100644 --- a/server.ts +++ b/server.ts @@ -180,6 +180,15 @@ export function app() { changeOrigin: true })); + /** + * Proxy the linksets + */ + router.use('/linksets**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/linksets`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); + /** * Checks if the rateLimiter property is present * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. diff --git a/src/app/core/metadata-item/metadata-item.service.spec.ts b/src/app/core/metadata-item/metadata-item.service.spec.ts new file mode 100644 index 0000000000..89ca15658d --- /dev/null +++ b/src/app/core/metadata-item/metadata-item.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MetadataItemService } from './metadata-item.service'; + +describe('MetadataItemService', () => { + let service: MetadataItemService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MetadataItemService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/metadata-item/metadata-item.service.ts b/src/app/core/metadata-item/metadata-item.service.ts new file mode 100644 index 0000000000..a4fbcf587b --- /dev/null +++ b/src/app/core/metadata-item/metadata-item.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@angular/core'; +import { MetadataService } from '../metadata/metadata.service'; +import { ActivatedRoute, NavigationEnd, Event as NavigationEvent, NavigationStart, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { BundleDataService } from '../data/bundle-data.service'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; +import { RootDataService } from '../data/root-data.service'; +import { CoreState } from '../core-state.model'; +import { Store } from '@ngrx/store'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; +import { DOCUMENT } from '@angular/common'; + +@Injectable({ + providedIn: 'root' +}) +export class MetadataItemService extends MetadataService { + + constructor( + private router1: ActivatedRoute, + router: Router, + translate: TranslateService, + meta: Meta, + title: Title, + dsoNameService: DSONameService, + bundleDataService: BundleDataService, + bitstreamDataService: BitstreamDataService, + bitstreamFormatDataService: BitstreamFormatDataService, + rootService: RootDataService, + store: Store, + hardRedirectService: HardRedirectService, + @Inject(APP_CONFIG) appConfig: AppConfig, + authorizationService: AuthorizationDataService, + @Inject(DOCUMENT) private document: Document + ) { + super(router, translate, meta, title, dsoNameService, bundleDataService, bitstreamDataService, bitstreamFormatDataService, rootService, store, hardRedirectService, appConfig, authorizationService); + } + + public checkCurrentRoute(){ + + console.log(this.router); + + this.router1.url.subscribe(url => { + console.log(url); + console.log(url[0].path); + }); + + // this.router.events.subscribe((event: NavigationEvent) => { + // if(event instanceof NavigationStart) { + // if(event.url.startsWith('/entities')){ + // console.log('We are on ENTITIES!'); + // } + // } + // }); + } + + setLinkTag(){ + this.clearMetaTags(); + + let link: HTMLLinkElement = this.document.createElement('link'); + link.setAttribute('rel', ''); + link.setAttribute('href', ''); + this.document.head.appendChild(link); + } +} diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 204c925e6b..c46f8b1d6e 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -84,7 +84,7 @@ export class MetadataService { ]; constructor( - private router: Router, + protected router: Router, private translate: TranslateService, private meta: Meta, private title: Title, @@ -363,6 +363,15 @@ export class MetadataService { } } + /** + * Add to the + */ + // private setLinkTag(): void { + // const value = this.getMetaTagValue('dc.link'); + // this.meta.addTag({ name: 'Link', content: value }); + // this.addMetaTag('Link', value); + // } + getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { return observableOf(bitstream).pipe( getDownloadableBitstream(this.authorizationService), diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 9fef2ca4ed..d5978d782d 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -24,6 +24,7 @@ import { isAuthenticationBlocking } from './core/auth/selectors'; import { distinctUntilChanged, find } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { MenuService } from './shared/menu/menu.service'; +import { MetadataItemService } from './core/metadata-item/metadata-item.service'; /** * Performs the initialization of the app. @@ -50,6 +51,7 @@ export abstract class InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService, @@ -188,6 +190,7 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); + this.metadataItem.checkCurrentRoute(); } /** diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 61d57f10f9..f38883be1e 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -32,6 +32,7 @@ import { logStartupMessage } from '../../../startup-message'; import { MenuService } from '../../app/shared/menu/menu.service'; import { RootDataService } from '../../app/core/data/root-data.service'; import { firstValueFrom, Subscription } from 'rxjs'; +import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs client-side initialization. @@ -51,6 +52,7 @@ export class BrowserInitService extends InitService { protected angulartics2DSpace: Angulartics2DSpace, protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected klaroService: KlaroService, protected authService: AuthService, @@ -66,6 +68,7 @@ export class BrowserInitService extends InitService { localeService, angulartics2DSpace, metadata, + metadataItem, breadcrumbsService, themeService, menuService, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index d909bb0e8d..074efc31e7 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -21,6 +21,7 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { take } from 'rxjs/operators'; import { MenuService } from '../../app/shared/menu/menu.service'; +import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs server-side initialization. @@ -36,9 +37,10 @@ export class ServerInitService extends InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, + protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, - protected menuService: MenuService, + protected menuService: MenuService ) { super( store, @@ -48,6 +50,7 @@ export class ServerInitService extends InitService { localeService, angulartics2DSpace, metadata, + metadataItem, breadcrumbsService, themeService, menuService, From e8ff0fbf3638f419a0673ccd7ab6838a53ebe88c Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 11 May 2023 12:53:18 +0200 Subject: [PATCH 02/15] [CST-5729] implemented functionality to add link tags in head html section when on item page --- server.ts | 12 ++-- .../data/signposting-data.service.spec.ts | 16 +++++ src/app/core/data/signposting-data.service.ts | 52 ++++++++++++++ src/app/core/data/signposting-data.ts | 5 ++ .../core/dspace-rest/dspace-rest.service.ts | 15 +++- .../metadata-item.service.spec.ts | 16 ----- .../metadata-item/metadata-item.service.ts | 70 ------------------- src/app/core/metadata/metadata.service.ts | 56 ++++++++++++--- src/app/init.service.ts | 4 +- src/modules/app/browser-init.service.ts | 3 - src/modules/app/server-init.service.ts | 3 - 11 files changed, 139 insertions(+), 113 deletions(-) create mode 100644 src/app/core/data/signposting-data.service.spec.ts create mode 100644 src/app/core/data/signposting-data.service.ts create mode 100644 src/app/core/data/signposting-data.ts delete mode 100644 src/app/core/metadata-item/metadata-item.service.spec.ts delete mode 100644 src/app/core/metadata-item/metadata-item.service.ts diff --git a/server.ts b/server.ts index 5a3660e4de..b8796eb05d 100644 --- a/server.ts +++ b/server.ts @@ -180,14 +180,14 @@ export function app() { changeOrigin: true })); - /** + /** * Proxy the linksets */ - router.use('/linksets**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/linksets`, - pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true - })); + router.use('/linkset**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/signposting/linksets`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); /** * Checks if the rateLimiter property is present diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts new file mode 100644 index 0000000000..45c0abb29b --- /dev/null +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SignpostingDataService } from './signposting-data.service'; + +describe('SignpostingDataService', () => { + let service: SignpostingDataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SignpostingDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts new file mode 100644 index 0000000000..25bac49f17 --- /dev/null +++ b/src/app/core/data/signposting-data.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +// import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; +// import { throwError } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { HttpHeaders } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class SignpostingDataService { + + constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + + getLinks(uuid: string): Observable { + const url = this.halService.getRootHref().split('/'); + const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + + return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( + catchError((err ) => { + console.error(err); + return observableOf(false); + }), + map((res: RawRestResponse) => res) + ); + } + + getLinksets(uuid: string): Observable { + const url = this.halService.getRootHref().split('/'); + const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + + const requestOptions = { + observe: 'response' as any, + headers: new HttpHeaders({ + 'accept': 'application/linkset', + 'Content-Type': 'application/linkset' + }), + responseType: 'text' + } as any; + + return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( + catchError((err ) => { + console.error(err); + return observableOf(false); + }), + map((res: RawRestResponse) => res) + ); + } +} diff --git a/src/app/core/data/signposting-data.ts b/src/app/core/data/signposting-data.ts new file mode 100644 index 0000000000..5734d324ec --- /dev/null +++ b/src/app/core/data/signposting-data.ts @@ -0,0 +1,5 @@ +export interface SignpostingDataLink { + href: string, + rel: string, + type: string +} diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts index ea4e8c2831..737714869d 100644 --- a/src/app/core/dspace-rest/dspace-rest.service.ts +++ b/src/app/core/dspace-rest/dspace-rest.service.ts @@ -1,4 +1,4 @@ -import { Observable, throwError as observableThrowError } from 'rxjs'; +import { Observable, throwError as observableThrowError, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; @@ -58,6 +58,19 @@ export class DspaceRestService { })); } + getWithHeaders(absoluteURL: string, reqOptions: any): Observable { + const requestOptions = reqOptions; + + return this.http.get(absoluteURL, requestOptions).pipe( + map((res) => ({ + payload: res + })), + catchError((err) => { + console.log('Error: ', err); + return throwError(() => new Error(err.error)); + })); + } + /** * Performs a request to the REST API. * diff --git a/src/app/core/metadata-item/metadata-item.service.spec.ts b/src/app/core/metadata-item/metadata-item.service.spec.ts deleted file mode 100644 index 89ca15658d..0000000000 --- a/src/app/core/metadata-item/metadata-item.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { MetadataItemService } from './metadata-item.service'; - -describe('MetadataItemService', () => { - let service: MetadataItemService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(MetadataItemService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/core/metadata-item/metadata-item.service.ts b/src/app/core/metadata-item/metadata-item.service.ts deleted file mode 100644 index a4fbcf587b..0000000000 --- a/src/app/core/metadata-item/metadata-item.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { MetadataService } from '../metadata/metadata.service'; -import { ActivatedRoute, NavigationEnd, Event as NavigationEvent, NavigationStart, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { Meta, Title } from '@angular/platform-browser'; -import { DSONameService } from '../breadcrumbs/dso-name.service'; -import { BundleDataService } from '../data/bundle-data.service'; -import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; -import { RootDataService } from '../data/root-data.service'; -import { CoreState } from '../core-state.model'; -import { Store } from '@ngrx/store'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; -import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; -import { DOCUMENT } from '@angular/common'; - -@Injectable({ - providedIn: 'root' -}) -export class MetadataItemService extends MetadataService { - - constructor( - private router1: ActivatedRoute, - router: Router, - translate: TranslateService, - meta: Meta, - title: Title, - dsoNameService: DSONameService, - bundleDataService: BundleDataService, - bitstreamDataService: BitstreamDataService, - bitstreamFormatDataService: BitstreamFormatDataService, - rootService: RootDataService, - store: Store, - hardRedirectService: HardRedirectService, - @Inject(APP_CONFIG) appConfig: AppConfig, - authorizationService: AuthorizationDataService, - @Inject(DOCUMENT) private document: Document - ) { - super(router, translate, meta, title, dsoNameService, bundleDataService, bitstreamDataService, bitstreamFormatDataService, rootService, store, hardRedirectService, appConfig, authorizationService); - } - - public checkCurrentRoute(){ - - console.log(this.router); - - this.router1.url.subscribe(url => { - console.log(url); - console.log(url[0].path); - }); - - // this.router.events.subscribe((event: NavigationEvent) => { - // if(event instanceof NavigationStart) { - // if(event.url.startsWith('/entities')){ - // console.log('We are on ENTITIES!'); - // } - // } - // }); - } - - setLinkTag(){ - this.clearMetaTags(); - - let link: HTMLLinkElement = this.document.createElement('link'); - link.setAttribute('rel', ''); - link.setAttribute('href', ''); - this.document.head.appendChild(link); - } -} diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index c46f8b1d6e..6d5ca91b8d 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -45,6 +45,7 @@ import { CoreState } from '../core-state.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; +import { SignpostingDataService } from '../data/signposting-data.service'; /** * The base selector function to select the metaTag section in the store @@ -96,7 +97,8 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, - private authorizationService: AuthorizationDataService + private authorizationService: AuthorizationDataService, + private signpostginDataService: SignpostingDataService ) { } @@ -138,7 +140,7 @@ export class MetadataService { } } - private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + public getCurrentRoute(route: ActivatedRoute): ActivatedRoute { while (route.firstChild) { route = route.firstChild; } @@ -162,6 +164,8 @@ export class MetadataService { this.setCitationAbstractUrlTag(); this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); + this.setSignpostingLinks(); + this.setSignpostingLinksets(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -184,6 +188,45 @@ export class MetadataService { } + /** + * Add to the + */ + private setSignpostingLinks() { + if (this.currentObject.value instanceof Item){ + const value = this.signpostginDataService.getLinks(this.currentObject.getValue().id); + value.subscribe(links => { + links.payload.forEach(link => { + this.setLinkTag(link.href, link.rel, link.type); + }); + }); + } + } + + setLinkTag(href: string, rel: string, type: string){ + let link: HTMLLinkElement = document.createElement('link'); + link.href = href; + link.rel = rel; + link.type = type; + document.head.appendChild(link); + console.log(link); + } + + private setSignpostingLinksets() { + if (this.currentObject.value instanceof Item){ + const value = this.signpostginDataService.getLinksets(this.currentObject.getValue().id); + value.subscribe(linksets => { + this.setLinkAttribute(linksets); + }); + } + } + + setLinkAttribute(linksets){ + console.log('ANDREA', linksets); + const linkAttribute = `Link: ${linksets.payload.body}`; + const textNode = document.createTextNode(linkAttribute); + document.head.appendChild(textNode); + } + /** * Add to the */ @@ -363,15 +406,6 @@ export class MetadataService { } } - /** - * Add to the - */ - // private setLinkTag(): void { - // const value = this.getMetaTagValue('dc.link'); - // this.meta.addTag({ name: 'Link', content: value }); - // this.addMetaTag('Link', value); - // } - getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { return observableOf(bitstream).pipe( getDownloadableBitstream(this.authorizationService), diff --git a/src/app/init.service.ts b/src/app/init.service.ts index d5978d782d..2bbc589cc0 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -24,7 +24,6 @@ import { isAuthenticationBlocking } from './core/auth/selectors'; import { distinctUntilChanged, find } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { MenuService } from './shared/menu/menu.service'; -import { MetadataItemService } from './core/metadata-item/metadata-item.service'; /** * Performs the initialization of the app. @@ -51,7 +50,6 @@ export abstract class InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService, @@ -190,7 +188,7 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); - this.metadataItem.checkCurrentRoute(); + // this.metadataItem.checkCurrentRoute(); } /** diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index f38883be1e..61d57f10f9 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -32,7 +32,6 @@ import { logStartupMessage } from '../../../startup-message'; import { MenuService } from '../../app/shared/menu/menu.service'; import { RootDataService } from '../../app/core/data/root-data.service'; import { firstValueFrom, Subscription } from 'rxjs'; -import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs client-side initialization. @@ -52,7 +51,6 @@ export class BrowserInitService extends InitService { protected angulartics2DSpace: Angulartics2DSpace, protected googleAnalyticsService: GoogleAnalyticsService, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected klaroService: KlaroService, protected authService: AuthService, @@ -68,7 +66,6 @@ export class BrowserInitService extends InitService { localeService, angulartics2DSpace, metadata, - metadataItem, breadcrumbsService, themeService, menuService, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 074efc31e7..715f872cd9 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -21,7 +21,6 @@ import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { take } from 'rxjs/operators'; import { MenuService } from '../../app/shared/menu/menu.service'; -import { MetadataItemService } from 'src/app/core/metadata-item/metadata-item.service'; /** * Performs server-side initialization. @@ -37,7 +36,6 @@ export class ServerInitService extends InitService { protected localeService: LocaleService, protected angulartics2DSpace: Angulartics2DSpace, protected metadata: MetadataService, - protected metadataItem: MetadataItemService, protected breadcrumbsService: BreadcrumbsService, protected themeService: ThemeService, protected menuService: MenuService @@ -50,7 +48,6 @@ export class ServerInitService extends InitService { localeService, angulartics2DSpace, metadata, - metadataItem, breadcrumbsService, themeService, menuService, From fe8bbddac25a40be99193a98232c1bf8ba334968 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 May 2023 15:05:35 +0200 Subject: [PATCH 03/15] [CST-5729] Fix proxying for signposting --- server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server.ts b/server.ts index b8796eb05d..282b1ce29a 100644 --- a/server.ts +++ b/server.ts @@ -26,7 +26,6 @@ import * as ejs from 'ejs'; import * as compression from 'compression'; import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ - import axios from 'axios'; import LRU from 'lru-cache'; import isbot from 'isbot'; @@ -34,7 +33,7 @@ import { createCertificate } from 'pem'; import { createServer } from 'https'; import { json } from 'body-parser'; -import { existsSync, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'path'; import { enableProdMode } from '@angular/core'; @@ -183,8 +182,8 @@ export function app() { /** * Proxy the linksets */ - router.use('/linkset**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/signposting/linksets`, + router.use('/links**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/signposting`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), changeOrigin: true })); From 558f8f51fb8cd6a8a0d6547b1c660e5f1d8fc917 Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Thu, 11 May 2023 18:47:01 +0200 Subject: [PATCH 04/15] [CST-5729] Implemented functionality to add Links in Response Headers on Item Page --- src/app/core/data/signposting-data.service.ts | 10 +++--- src/app/core/metadata/metadata.service.ts | 32 ++++++++++--------- .../services/server-hard-redirect.service.ts | 12 +++++++ .../core/services/server-response.service.ts | 6 ++++ .../full/full-item-page.component.ts | 8 +++-- .../item-page/simple/item-page.component.ts | 11 ++++++- 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 25bac49f17..0ef2b49f0f 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -16,8 +16,7 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } getLinks(uuid: string): Observable { - const url = this.halService.getRootHref().split('/'); - const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err ) => { @@ -28,9 +27,8 @@ export class SignpostingDataService { ); } - getLinksets(uuid: string): Observable { - const url = this.halService.getRootHref().split('/'); - const baseUrl = `${url[0]}//${url[2]}/${url[3]}`; + getLinksets(uuid: string): Observable { + const baseUrl = this.halService.getRootHref().replace('/api', ''); const requestOptions = { observe: 'response' as any, @@ -46,7 +44,7 @@ export class SignpostingDataService { console.error(err); return observableOf(false); }), - map((res: RawRestResponse) => res) + map((res: RawRestResponse) => res.payload.body) ); } } diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 6d5ca91b8d..3cc678fb15 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -165,7 +165,6 @@ export class MetadataService { this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); this.setSignpostingLinks(); - this.setSignpostingLinksets(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -211,21 +210,24 @@ export class MetadataService { console.log(link); } - private setSignpostingLinksets() { - if (this.currentObject.value instanceof Item){ - const value = this.signpostginDataService.getLinksets(this.currentObject.getValue().id); - value.subscribe(linksets => { - this.setLinkAttribute(linksets); - }); - } - } + // public setSignpostingLinksets(itemId: string) { + // let linkSet: string; - setLinkAttribute(linksets){ - console.log('ANDREA', linksets); - const linkAttribute = `Link: ${linksets.payload.body}`; - const textNode = document.createTextNode(linkAttribute); - document.head.appendChild(textNode); - } + // const value = this.signpostginDataService.getLinksets(itemId); + + // value.subscribe(linksets => { + // linkSet = linksets.payload.body; + // }); + + // return linkSet; + // } + + // setLinkAttribute(linksets){ + // console.log('ANDREA', linksets); + // const linkAttribute = `Link: ${linksets.payload.body}`; + // const textNode = document.createTextNode(linkAttribute); + // document.head.appendChild(textNode); + // } /** * Add to the diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts index 94b9ed6198..a6c0e09aee 100644 --- a/src/app/core/services/server-hard-redirect.service.ts +++ b/src/app/core/services/server-hard-redirect.service.ts @@ -2,6 +2,9 @@ import { Inject, Injectable } from '@angular/core'; import { Request, Response } from 'express'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { HardRedirectService } from './hard-redirect.service'; +import { SignpostingDataService } from '../data/signposting-data.service'; +import { ActivatedRoute } from '@angular/router'; +import { take } from 'rxjs'; /** * Service for performing hard redirects within the server app module @@ -12,6 +15,8 @@ export class ServerHardRedirectService extends HardRedirectService { constructor( @Inject(REQUEST) protected req: Request, @Inject(RESPONSE) protected res: Response, + private signpostginDataService: SignpostingDataService, + protected route: ActivatedRoute ) { super(); } @@ -46,6 +51,13 @@ export class ServerHardRedirectService extends HardRedirectService { } console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`); + + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { + this.res.setHeader('Link', linksets); + }); + }); + this.res.redirect(status, url); this.res.end(); // I haven't found a way to correctly stop Angular rendering. diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 02e00446bc..6dd50506e9 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -35,4 +35,10 @@ export class ServerResponseService { setInternalServerError(message = 'Internal Server Error'): this { return this.setStatus(500, message); } + + setLinksetsHeader(linksets: string){ + if (this.response) { + this.response.setHeader('Link', linksets); + } + } } 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 118e436004..44766bac7b 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -16,6 +16,8 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ServerResponseService } from 'src/app/core/services/server-response.service'; +import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; /** @@ -48,8 +50,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, items: ItemDataService, authService: AuthService, authorizationService: AuthorizationDataService, - private _location: Location) { - super(route, router, items, authService, authorizationService); + private _location: Location, + responseService: ServerResponseService, + signpostginDataService: SignpostingDataService) { + super(route, router, items, authService, authorizationService, responseService, signpostginDataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 6e0db386db..058cbc667a 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -15,6 +15,8 @@ import { getItemPageRoute } from '../item-page-routing-paths'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; /** * This component renders a simple item page. @@ -62,8 +64,15 @@ export class ItemPageComponent implements OnInit { private router: Router, private items: ItemDataService, private authService: AuthService, - private authorizationService: AuthorizationDataService + private authorizationService: AuthorizationDataService, + private responseService: ServerResponseService, + private signpostginDataService: SignpostingDataService ) { + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).subscribe(linksets => { + this.responseService.setLinksetsHeader(linksets); + }); + }); } /** From 6f4b0ad6b1b03af129f1f3c80c3e8b3c2a419d04 Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Fri, 12 May 2023 12:10:57 +0200 Subject: [PATCH 05/15] [CST-5729] fixed the id on the bitstream api request --- .../bitstream-download-page.component.ts | 10 +++++++++- src/app/core/data/signposting-data.service.ts | 9 +++++---- src/app/core/data/signposting-data.ts | 5 ----- src/app/core/data/signposting-links.model.ts | 7 +++++++ src/app/core/services/server-hard-redirect.service.ts | 11 ----------- 5 files changed, 21 insertions(+), 21 deletions(-) delete mode 100644 src/app/core/data/signposting-data.ts create mode 100644 src/app/core/data/signposting-links.model.ts diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 51ec762ec3..38e4d01dd5 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -14,6 +14,8 @@ import { getForbiddenRoute } from '../../app-routing-paths'; import { RemoteData } from '../../core/data/remote-data'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { Location } from '@angular/common'; +import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; +import { ServerResponseService } from 'src/app/core/services/server-response.service'; @Component({ selector: 'ds-bitstream-download-page', @@ -36,8 +38,14 @@ export class BitstreamDownloadPageComponent implements OnInit { private fileService: FileService, private hardRedirectService: HardRedirectService, private location: Location, + private signpostginDataService: SignpostingDataService, + private responseService: ServerResponseService ) { - + this.route.params.subscribe(params => { + this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { + this.responseService.setLinksetsHeader(linksets); + }); + }); } back(): void { diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 0ef2b49f0f..5e965d1ab5 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -7,6 +7,7 @@ import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { HttpHeaders } from '@angular/common/http'; +import { SignpostingLinks } from './signposting-links.model'; @Injectable({ providedIn: 'root' @@ -15,15 +16,15 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( - catchError((err ) => { + catchError((err) => { console.error(err); return observableOf(false); }), - map((res: RawRestResponse) => res) + map((res: SignpostingLinks) => res) ); } @@ -40,7 +41,7 @@ export class SignpostingDataService { } as any; return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( - catchError((err ) => { + catchError((err) => { console.error(err); return observableOf(false); }), diff --git a/src/app/core/data/signposting-data.ts b/src/app/core/data/signposting-data.ts deleted file mode 100644 index 5734d324ec..0000000000 --- a/src/app/core/data/signposting-data.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface SignpostingDataLink { - href: string, - rel: string, - type: string -} diff --git a/src/app/core/data/signposting-links.model.ts b/src/app/core/data/signposting-links.model.ts new file mode 100644 index 0000000000..19d8869ba1 --- /dev/null +++ b/src/app/core/data/signposting-links.model.ts @@ -0,0 +1,7 @@ +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; + +export interface SignpostingLinks extends RawRestResponse { + href?: string, + rel?: string, + type?: string +} diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts index a6c0e09aee..de8b45b0e5 100644 --- a/src/app/core/services/server-hard-redirect.service.ts +++ b/src/app/core/services/server-hard-redirect.service.ts @@ -2,9 +2,6 @@ import { Inject, Injectable } from '@angular/core'; import { Request, Response } from 'express'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { HardRedirectService } from './hard-redirect.service'; -import { SignpostingDataService } from '../data/signposting-data.service'; -import { ActivatedRoute } from '@angular/router'; -import { take } from 'rxjs'; /** * Service for performing hard redirects within the server app module @@ -15,8 +12,6 @@ export class ServerHardRedirectService extends HardRedirectService { constructor( @Inject(REQUEST) protected req: Request, @Inject(RESPONSE) protected res: Response, - private signpostginDataService: SignpostingDataService, - protected route: ActivatedRoute ) { super(); } @@ -52,12 +47,6 @@ export class ServerHardRedirectService extends HardRedirectService { console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`); - this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { - this.res.setHeader('Link', linksets); - }); - }); - this.res.redirect(status, url); this.res.end(); // I haven't found a way to correctly stop Angular rendering. From 22fbcbebfa793054bd8f61a95d3c67e8f05940ec Mon Sep 17 00:00:00 2001 From: Alban Imami Date: Fri, 12 May 2023 16:01:19 +0200 Subject: [PATCH 06/15] [CST-5729] Method to remove Link tags from Head when switched page --- src/app/core/metadata/metadata.service.ts | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 3cc678fb15..60409f17a1 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -64,6 +64,11 @@ const tagsInUseSelector = (state: MetaTagState) => state.tagsInUse, ); +/** + * Link elements added on Item Page + */ +let linkTags = []; + @Injectable() export class MetadataService { @@ -119,6 +124,7 @@ export class MetadataService { private processRouteChange(routeInfo: any): void { this.clearMetaTags(); + this.clearLinkTags(); if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { this.currentObject.next(routeInfo.data.value.dso.payload); @@ -207,27 +213,14 @@ export class MetadataService { link.rel = rel; link.type = type; document.head.appendChild(link); - console.log(link); + linkTags.push(link); } - // public setSignpostingLinksets(itemId: string) { - // let linkSet: string; - - // const value = this.signpostginDataService.getLinksets(itemId); - - // value.subscribe(linksets => { - // linkSet = linksets.payload.body; - // }); - - // return linkSet; - // } - - // setLinkAttribute(linksets){ - // console.log('ANDREA', linksets); - // const linkAttribute = `Link: ${linksets.payload.body}`; - // const textNode = document.createTextNode(linkAttribute); - // document.head.appendChild(textNode); - // } + public clearLinkTags(){ + linkTags.forEach(link => { + link.parentNode.removeChild(link); + }); + } /** * Add to the From 93d9b87db16e939308d9faac7462bc57ec5cda7b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 12 May 2023 19:52:47 +0200 Subject: [PATCH 07/15] [CST-5729] Finalize implementation --- .../bitstream-download-page.component.spec.ts | 35 ++++++--- .../bitstream-download-page.component.ts | 9 ++- .../data/signposting-data.service.spec.ts | 74 ++++++++++++++++++- src/app/core/data/signposting-data.service.ts | 9 +-- .../core/metadata/metadata.service.spec.ts | 24 ++++-- src/app/core/metadata/metadata.service.ts | 39 +++++----- .../full/full-item-page.component.spec.ts | 19 ++++- .../full/full-item-page.component.ts | 5 +- .../simple/item-page.component.spec.ts | 14 ++++ .../item-page/simple/item-page.component.ts | 4 +- 10 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index e84b254eae..71fd74a707 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -11,6 +11,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { getForbiddenRoute } from '../../app-routing-paths'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; describe('BitstreamDownloadPageComponent', () => { let component: BitstreamDownloadPageComponent; @@ -24,6 +26,8 @@ describe('BitstreamDownloadPageComponent', () => { let router; let bitstream: Bitstream; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; function init() { authService = jasmine.createSpyObj('authService', { @@ -44,8 +48,8 @@ describe('BitstreamDownloadPageComponent', () => { bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', _links: { - content: {href: 'bitstream-content-link'}, - self: {href: 'bitstream-self-link'}, + content: { href: 'bitstream-content-link' }, + self: { href: 'bitstream-self-link' }, } }); @@ -54,10 +58,21 @@ describe('BitstreamDownloadPageComponent', () => { bitstream: createSuccessfulRemoteDataObject( bitstream ) + }), + params: observableOf({ + id: 'testid' }) }; router = jasmine.createSpyObj('router', ['navigateByUrl']); + + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: observableOf('test'), + }); } function initTestbed() { @@ -65,12 +80,14 @@ describe('BitstreamDownloadPageComponent', () => { imports: [CommonModule, TranslateModule.forRoot()], declarations: [BitstreamDownloadPageComponent], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: Router, useValue: router}, - {provide: AuthorizationDataService, useValue: authorizationService}, - {provide: AuthService, useValue: authService}, - {provide: FileService, useValue: fileService}, - {provide: HardRedirectService, useValue: hardRedirectService}, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: AuthService, useValue: authService }, + { provide: FileService, useValue: fileService }, + { provide: HardRedirectService, useValue: hardRedirectService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService } ] }) .compileComponents(); @@ -134,7 +151,7 @@ describe('BitstreamDownloadPageComponent', () => { fixture.detectChanges(); }); it('should navigate to the forbidden route', () => { - expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true}); + expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true }); }); }); describe('when the user is not authorized and not logged in', () => { diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 38e4d01dd5..4814a9385a 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getRemoteDataPayload} from '../../core/shared/operators'; +import { getRemoteDataPayload } from '../../core/shared/operators'; import { Bitstream } from '../../core/shared/bitstream.model'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; @@ -29,7 +29,6 @@ export class BitstreamDownloadPageComponent implements OnInit { bitstream$: Observable; bitstreamRD$: Observable>; - constructor( private route: ActivatedRoute, protected router: Router, @@ -42,8 +41,10 @@ export class BitstreamDownloadPageComponent implements OnInit { private responseService: ServerResponseService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { - this.responseService.setLinksetsHeader(linksets); + this.signpostginDataService.getLinks(params.id).pipe(take(1)).subscribe(linksets => { + linksets.forEach(link => { + this.responseService.setLinksetsHeader(link.href); + }); }); }); } diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index 45c0abb29b..091f38de2a 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -1,16 +1,84 @@ -import { TestBed } from '@angular/core/testing'; - +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { SignpostingDataService } from './signposting-data.service'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { of } from 'rxjs'; +import { SignpostingLinks } from './signposting-links.model'; +import { map } from 'rxjs/operators'; describe('SignpostingDataService', () => { let service: SignpostingDataService; + let restServiceSpy: jasmine.SpyObj; + let halServiceSpy: jasmine.SpyObj; beforeEach(() => { - TestBed.configureTestingModule({}); + const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); + const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']); + + TestBed.configureTestingModule({ + providers: [ + SignpostingDataService, + { provide: DspaceRestService, useValue: restSpy }, + { provide: HALEndpointService, useValue: halSpy } + ] + }); + service = TestBed.inject(SignpostingDataService); + restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj; + halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj; }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should return signposting links', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + const mockResponse: any = { + self: { + href: `${baseUrl}/signposting/links/${uuid}` + } + }; + + restServiceSpy.get.and.returnValue(of(mockResponse)); + + let result: SignpostingLinks; + + service.getLinks(uuid).subscribe((links) => { + result = links; + }); + + tick(); + + expect(result).toEqual(mockResponse); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); + + it('should handle error and return false', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + restServiceSpy.get.and.returnValue(of(null).pipe(map(() => { + throw new Error('Error'); + }))); + + let result: any; + + service.getLinks(uuid).subscribe((data) => { + result = data; + }); + + tick(); + + expect(result).toBeFalse(); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); }); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 5e965d1ab5..efdf80a381 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -// import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { catchError, map } from 'rxjs/operators'; -// import { throwError } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; @@ -14,9 +12,10 @@ import { SignpostingLinks } from './signposting-links.model'; }) export class SignpostingDataService { - constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { + } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( @@ -24,7 +23,7 @@ export class SignpostingDataService { console.error(err); return observableOf(false); }), - map((res: SignpostingLinks) => res) + map((res: RawRestResponse) => res.payload as SignpostingLinks[]) ); } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 553b437d71..3c5b4adc0c 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,12 +8,7 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { - ItemMock, - MockBitstream1, - MockBitstream3, - MockBitstream2 -} from '../../shared/mocks/item.mock'; +import { ItemMock, MockBitstream1, MockBitstream2, MockBitstream3 } from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -30,6 +25,7 @@ import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; +import { SignpostingDataService } from '../data/signposting-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -46,6 +42,7 @@ describe('MetadataService', () => { let translateService: TranslateService; let hardRedirectService: HardRedirectService; let authorizationService: AuthorizationDataService; + let signpostingDataService: SignpostingDataService; let router: Router; let store; @@ -53,7 +50,12 @@ describe('MetadataService', () => { let appConfig: AppConfig; const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; - + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + const document: any = jasmine.createSpyObj('document', ['head', 'createElement']); beforeEach(() => { rootService = jasmine.createSpyObj({ @@ -90,6 +92,10 @@ describe('MetadataService', () => { isAuthorized: observableOf(true) }); + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink]) + }); + // @ts-ignore store = getMockStore({ initialState }); spyOn(store, 'dispatch'); @@ -115,7 +121,9 @@ describe('MetadataService', () => { store, hardRedirectService, appConfig, - authorizationService + document, + authorizationService, + signpostingDataService ); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 60409f17a1..c22f14b680 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -8,12 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - Observable, - of as observableOf, concat as observableConcat, - EMPTY + EMPTY, + Observable, + of as observableOf } from 'rxjs'; -import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; +import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; @@ -25,10 +25,7 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload -} from '../shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -46,6 +43,7 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { SignpostingDataService } from '../data/signposting-data.service'; +import { DOCUMENT } from '@angular/common'; /** * The base selector function to select the metaTag section in the store @@ -102,8 +100,9 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, + @Inject(DOCUMENT) private _document: Document, private authorizationService: AuthorizationDataService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { } @@ -198,9 +197,9 @@ export class MetadataService { */ private setSignpostingLinks() { if (this.currentObject.value instanceof Item){ - const value = this.signpostginDataService.getLinks(this.currentObject.getValue().id); + const value = this.signpostingDataService.getLinks(this.currentObject.getValue().id); value.subscribe(links => { - links.payload.forEach(link => { + links.forEach(link => { this.setLinkTag(link.href, link.rel, link.type); }); }); @@ -208,17 +207,19 @@ export class MetadataService { } setLinkTag(href: string, rel: string, type: string){ - let link: HTMLLinkElement = document.createElement('link'); - link.href = href; - link.rel = rel; - link.type = type; - document.head.appendChild(link); - linkTags.push(link); + let link: HTMLLinkElement = this._document.createElement('link'); + if (link) { + link.href = href; + link.rel = rel; + link.type = type; + this._document.head?.appendChild(link); + linkTags.push(link); + } } public clearLinkTags(){ linkTags.forEach(link => { - link.parentNode.removeChild(link); + link.parentNode?.removeChild(link); }); } 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 53e36be1d1..6c59ccbc67 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 @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf, of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -20,6 +20,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; import { RemoteData } from '../../core/data/remote-data'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -55,8 +57,8 @@ describe('FullItemPageComponent', () => { let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; - - + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -76,6 +78,14 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -90,8 +100,9 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], - schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } 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 44766bac7b..2570bf70fe 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -19,7 +19,6 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { ServerResponseService } from 'src/app/core/services/server-response.service'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; - /** * This component renders a full item page. * The route parameter 'id' is used to request the item it represents. @@ -52,8 +51,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, authorizationService: AuthorizationDataService, private _location: Location, responseService: ServerResponseService, - signpostginDataService: SignpostingDataService) { - super(route, router, items, authService, authorizationService, responseService, signpostginDataService); + signpostingDataService: SignpostingDataService) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ 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 9b0e87939d..042cac2724 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -22,6 +22,9 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { of } from 'rxjs/internal/observable/of'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -41,6 +44,8 @@ describe('ItemPageComponent', () => { let fixture: ComponentFixture; let authService: AuthService; let authorizationDataService: AuthorizationDataService; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -60,6 +65,13 @@ describe('ItemPageComponent', () => { authorizationDataService = jasmine.createSpyObj('authorizationDataService', { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -76,6 +88,8 @@ describe('ItemPageComponent', () => { { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 058cbc667a..6483769483 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -66,10 +66,10 @@ export class ItemPageComponent implements OnInit { private authService: AuthService, private authorizationService: AuthorizationDataService, private responseService: ServerResponseService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).subscribe(linksets => { + this.signpostingDataService.getLinksets(params.id).subscribe(linksets => { this.responseService.setLinksetsHeader(linksets); }); }); From 78cf3e1bfc52b43b64023d8a0d75b1354dbc28cf Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:27:38 +0200 Subject: [PATCH 08/15] [CST-5729] Finalize signposting implementation --- .../bitstream-download-page.component.spec.ts | 19 +++++- .../bitstream-download-page.component.ts | 13 ++-- .../data/signposting-data.service.spec.ts | 43 ++++++++----- src/app/core/data/signposting-data.service.ts | 33 +++------- src/app/core/data/signposting-links.model.ts | 7 ++- .../core/metadata/metadata.service.spec.ts | 24 +++---- src/app/core/metadata/metadata.service.ts | 63 ++++--------------- .../core/services/server-response.service.ts | 5 +- .../full/full-item-page.component.spec.ts | 41 +++++++++++- .../full/full-item-page.component.ts | 26 ++++---- .../simple/item-page.component.spec.ts | 41 +++++++++++- .../item-page/simple/item-page.component.ts | 50 +++++++++++---- 12 files changed, 218 insertions(+), 147 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index 71fd74a707..66024063cd 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -29,6 +29,18 @@ describe('BitstreamDownloadPageComponent', () => { let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; + function init() { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), @@ -67,11 +79,11 @@ describe('BitstreamDownloadPageComponent', () => { router = jasmine.createSpyObj('router', ['navigateByUrl']); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: observableOf('test'), + getLinks: observableOf([mocklink, mocklink2]) }); } @@ -124,6 +136,9 @@ describe('BitstreamDownloadPageComponent', () => { it('should redirect to the content link', () => { expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link'); }); + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + }); }); describe('when the user is authorized and logged in', () => { beforeEach(waitForAsync(() => { diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 4814a9385a..14245c4cd1 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -16,6 +16,7 @@ import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { Location } from '@angular/common'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; import { ServerResponseService } from 'src/app/core/services/server-response.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; @Component({ selector: 'ds-bitstream-download-page', @@ -37,14 +38,18 @@ export class BitstreamDownloadPageComponent implements OnInit { private fileService: FileService, private hardRedirectService: HardRedirectService, private location: Location, - private signpostginDataService: SignpostingDataService, + private signpostingDataService: SignpostingDataService, private responseService: ServerResponseService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinks(params.id).pipe(take(1)).subscribe(linksets => { - linksets.forEach(link => { - this.responseService.setLinksetsHeader(link.href); + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; }); + + this.responseService.setHeader('Link', links); }); }); } diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index 091f38de2a..c76899221e 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -3,13 +3,32 @@ import { SignpostingDataService } from './signposting-data.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { of } from 'rxjs'; -import { SignpostingLinks } from './signposting-links.model'; -import { map } from 'rxjs/operators'; +import { SignpostingLink } from './signposting-links.model'; describe('SignpostingDataService', () => { let service: SignpostingDataService; let restServiceSpy: jasmine.SpyObj; let halServiceSpy: jasmine.SpyObj; + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; + + const mockResponse: any = { + statusCode: 200, + payload: [mocklink, mocklink2] + }; + + const mockErrResponse: any = { + statusCode: 500 + }; beforeEach(() => { const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); @@ -38,15 +57,11 @@ describe('SignpostingDataService', () => { halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - const mockResponse: any = { - self: { - href: `${baseUrl}/signposting/links/${uuid}` - } - }; - restServiceSpy.get.and.returnValue(of(mockResponse)); - let result: SignpostingLinks; + let result: SignpostingLink[]; + + const expectedResult: SignpostingLink[] = [mocklink, mocklink2]; service.getLinks(uuid).subscribe((links) => { result = links; @@ -54,20 +69,18 @@ describe('SignpostingDataService', () => { tick(); - expect(result).toEqual(mockResponse); + expect(result).toEqual(expectedResult); expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); - it('should handle error and return false', fakeAsync(() => { + it('should handle error and return an empty array', fakeAsync(() => { const uuid = '123'; const baseUrl = 'http://localhost:8080'; halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); - restServiceSpy.get.and.returnValue(of(null).pipe(map(() => { - throw new Error('Error'); - }))); + restServiceSpy.get.and.returnValue(of(mockErrResponse)); let result: any; @@ -77,7 +90,7 @@ describe('SignpostingDataService', () => { tick(); - expect(result).toBeFalse(); + expect(result).toEqual([]); expect(halServiceSpy.getRootHref).toHaveBeenCalled(); expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); })); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index efdf80a381..e09d68974c 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,11 +1,12 @@ import { Injectable } from '@angular/core'; -import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; + import { catchError, map } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; + +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { HttpHeaders } from '@angular/common/http'; -import { SignpostingLinks } from './signposting-links.model'; +import { SignpostingLink } from './signposting-links.model'; @Injectable({ providedIn: 'root' @@ -15,36 +16,16 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { console.error(err); - return observableOf(false); + return observableOf([]); }), - map((res: RawRestResponse) => res.payload as SignpostingLinks[]) + map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : []) ); } - getLinksets(uuid: string): Observable { - const baseUrl = this.halService.getRootHref().replace('/api', ''); - - const requestOptions = { - observe: 'response' as any, - headers: new HttpHeaders({ - 'accept': 'application/linkset', - 'Content-Type': 'application/linkset' - }), - responseType: 'text' - } as any; - - return this.restService.getWithHeaders(`${baseUrl}/signposting/linksets/${uuid}`, requestOptions).pipe( - catchError((err) => { - console.error(err); - return observableOf(false); - }), - map((res: RawRestResponse) => res.payload.body) - ); - } } diff --git a/src/app/core/data/signposting-links.model.ts b/src/app/core/data/signposting-links.model.ts index 19d8869ba1..11d2cafe00 100644 --- a/src/app/core/data/signposting-links.model.ts +++ b/src/app/core/data/signposting-links.model.ts @@ -1,6 +1,7 @@ -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; - -export interface SignpostingLinks extends RawRestResponse { +/** + * Represents the link object received by the signposting endpoint + */ +export interface SignpostingLink { href?: string, rel?: string, type?: string diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 3c5b4adc0c..553b437d71 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,7 +8,12 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { ItemMock, MockBitstream1, MockBitstream2, MockBitstream3 } from '../../shared/mocks/item.mock'; +import { + ItemMock, + MockBitstream1, + MockBitstream3, + MockBitstream2 +} from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -25,7 +30,6 @@ import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; -import { SignpostingDataService } from '../data/signposting-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -42,7 +46,6 @@ describe('MetadataService', () => { let translateService: TranslateService; let hardRedirectService: HardRedirectService; let authorizationService: AuthorizationDataService; - let signpostingDataService: SignpostingDataService; let router: Router; let store; @@ -50,12 +53,7 @@ describe('MetadataService', () => { let appConfig: AppConfig; const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; - const mocklink = { - href: 'http://test.org', - rel: 'test', - type: 'test' - }; - const document: any = jasmine.createSpyObj('document', ['head', 'createElement']); + beforeEach(() => { rootService = jasmine.createSpyObj({ @@ -92,10 +90,6 @@ describe('MetadataService', () => { isAuthorized: observableOf(true) }); - signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinks: observableOf([mocklink]) - }); - // @ts-ignore store = getMockStore({ initialState }); spyOn(store, 'dispatch'); @@ -121,9 +115,7 @@ describe('MetadataService', () => { store, hardRedirectService, appConfig, - document, - authorizationService, - signpostingDataService + authorizationService ); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index c22f14b680..204c925e6b 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -8,12 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - concat as observableConcat, - EMPTY, Observable, - of as observableOf + of as observableOf, + concat as observableConcat, + EMPTY } from 'rxjs'; -import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; @@ -25,7 +25,10 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload +} from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -42,8 +45,6 @@ import { CoreState } from '../core-state.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; -import { SignpostingDataService } from '../data/signposting-data.service'; -import { DOCUMENT } from '@angular/common'; /** * The base selector function to select the metaTag section in the store @@ -62,11 +63,6 @@ const tagsInUseSelector = (state: MetaTagState) => state.tagsInUse, ); -/** - * Link elements added on Item Page - */ -let linkTags = []; - @Injectable() export class MetadataService { @@ -88,7 +84,7 @@ export class MetadataService { ]; constructor( - protected router: Router, + private router: Router, private translate: TranslateService, private meta: Meta, private title: Title, @@ -100,9 +96,7 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, - @Inject(DOCUMENT) private _document: Document, - private authorizationService: AuthorizationDataService, - private signpostingDataService: SignpostingDataService + private authorizationService: AuthorizationDataService ) { } @@ -123,7 +117,6 @@ export class MetadataService { private processRouteChange(routeInfo: any): void { this.clearMetaTags(); - this.clearLinkTags(); if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { this.currentObject.next(routeInfo.data.value.dso.payload); @@ -145,7 +138,7 @@ export class MetadataService { } } - public getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { while (route.firstChild) { route = route.firstChild; } @@ -169,7 +162,6 @@ export class MetadataService { this.setCitationAbstractUrlTag(); this.setCitationPdfUrlTag(); this.setCitationPublisherTag(); - this.setSignpostingLinks(); if (this.isDissertation()) { this.setCitationDissertationNameTag(); @@ -192,37 +184,6 @@ export class MetadataService { } - /** - * Add to the - */ - private setSignpostingLinks() { - if (this.currentObject.value instanceof Item){ - const value = this.signpostingDataService.getLinks(this.currentObject.getValue().id); - value.subscribe(links => { - links.forEach(link => { - this.setLinkTag(link.href, link.rel, link.type); - }); - }); - } - } - - setLinkTag(href: string, rel: string, type: string){ - let link: HTMLLinkElement = this._document.createElement('link'); - if (link) { - link.href = href; - link.rel = rel; - link.type = type; - this._document.head?.appendChild(link); - linkTags.push(link); - } - } - - public clearLinkTags(){ - linkTags.forEach(link => { - link.parentNode?.removeChild(link); - }); - } - /** * Add to the */ diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 6dd50506e9..ffb6a204c6 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -36,9 +36,10 @@ export class ServerResponseService { return this.setStatus(500, message); } - setLinksetsHeader(linksets: string){ + setHeader(header: string, content: string) { + console.log(this.response); if (this.response) { - this.response.setHeader('Link', linksets); + this.response.setHeader(header, content); } } } 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 6c59ccbc67..ec4054d888 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 @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf, of } from 'rxjs'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -22,6 +22,7 @@ import { createRelationshipsObservable } from '../simple/item-types/shared/item. 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'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -59,6 +60,19 @@ describe('FullItemPageComponent', () => { let authorizationDataService: AuthorizationDataService; let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; + + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -79,11 +93,16 @@ describe('FullItemPageComponent', () => { }); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: of('test'), + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), }); TestBed.configureTestingModule({ @@ -102,6 +121,7 @@ describe('FullItemPageComponent', () => { { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { @@ -154,6 +174,11 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader.nativeElement).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -178,6 +203,11 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -190,5 +220,10 @@ describe('FullItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); expect(objectLoader).not.toBeNull(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); }); 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 2570bf70fe..f0100eed72 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -16,8 +16,9 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { ServerResponseService } from 'src/app/core/services/server-response.service'; -import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a full item page. @@ -44,15 +45,18 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, subs = []; - constructor(protected route: ActivatedRoute, - router: Router, - items: ItemDataService, - authService: AuthService, - authorizationService: AuthorizationDataService, - private _location: Location, - responseService: ServerResponseService, - signpostingDataService: SignpostingDataService) { - super(route, router, items, authService, authorizationService, responseService, signpostingDataService); + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected _location: Location, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService + ) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ 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 042cac2724..005142e3f1 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -22,9 +22,9 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { of } from 'rxjs/internal/observable/of'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -39,6 +39,18 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { isWithdrawn: true }); +const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' +}; + +const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' +}; + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; @@ -46,6 +58,7 @@ describe('ItemPageComponent', () => { let authorizationDataService: AuthorizationDataService; let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -66,11 +79,16 @@ describe('ItemPageComponent', () => { isAuthorized: observableOf(false), }); serverResponseService = jasmine.createSpyObj('ServerResponseService', { - setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + setHeader: jasmine.createSpy('setHeader'), }); signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { - getLinksets: of('test'), + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), }); TestBed.configureTestingModule({ @@ -90,6 +108,7 @@ describe('ItemPageComponent', () => { { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, ], schemas: [NO_ERRORS_SCHEMA] @@ -140,6 +159,12 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); + }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -164,6 +189,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -176,6 +206,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 6483769483..10d8b32886 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -17,6 +17,9 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { ServerResponseService } from '../../core/services/server-response.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a simple item page. @@ -30,7 +33,7 @@ import { SignpostingDataService } from '../../core/data/signposting-data.service changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) -export class ItemPageComponent implements OnInit { +export class ItemPageComponent implements OnInit, OnDestroy { /** * The item's id @@ -59,18 +62,36 @@ export class ItemPageComponent implements OnInit { itemUrl: string; + /** + * Contains a list of SignpostingLink related to the item + */ + signpostingLinks: SignpostingLink[]; + constructor( protected route: ActivatedRoute, - private router: Router, - private items: ItemDataService, - private authService: AuthService, - private authorizationService: AuthorizationDataService, - private responseService: ServerResponseService, - private signpostingDataService: SignpostingDataService + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService ) { this.route.params.subscribe(params => { - this.signpostingDataService.getLinksets(params.id).subscribe(linksets => { - this.responseService.setLinksetsHeader(linksets); + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + this.signpostingLinks = signpostingLinks; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + this.linkHeadService.addTag({ + href: link.href, + type: link.type, + rel: link.rel + }) + }); + + this.responseService.setHeader('Link', links); }); }); } @@ -91,4 +112,11 @@ export class ItemPageComponent implements OnInit { this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); } + + + ngOnDestroy(): void { + this.signpostingLinks.forEach((link: SignpostingLink) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }) + } } From a1b27a5fb361db209721bbc59e955782175699ec Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:34:02 +0200 Subject: [PATCH 09/15] [CST-5729] Revert unused changes --- src/app/core/dspace-rest/dspace-rest.service.ts | 15 +-------------- src/app/core/services/server-response.service.ts | 1 - 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts index 737714869d..ea4e8c2831 100644 --- a/src/app/core/dspace-rest/dspace-rest.service.ts +++ b/src/app/core/dspace-rest/dspace-rest.service.ts @@ -1,4 +1,4 @@ -import { Observable, throwError as observableThrowError, throwError } from 'rxjs'; +import { Observable, throwError as observableThrowError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; @@ -58,19 +58,6 @@ export class DspaceRestService { })); } - getWithHeaders(absoluteURL: string, reqOptions: any): Observable { - const requestOptions = reqOptions; - - return this.http.get(absoluteURL, requestOptions).pipe( - map((res) => ({ - payload: res - })), - catchError((err) => { - console.log('Error: ', err); - return throwError(() => new Error(err.error)); - })); - } - /** * Performs a request to the REST API. * diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index ffb6a204c6..2268e9eb03 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -37,7 +37,6 @@ export class ServerResponseService { } setHeader(header: string, content: string) { - console.log(this.response); if (this.response) { this.response.setHeader(header, content); } From 10c6b03c40af72e70a897d1fd460e8c39ea5ebdb Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 16:58:20 +0200 Subject: [PATCH 10/15] [CST-5729] Fix lint --- src/app/item-page/simple/item-page.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 10d8b32886..c1c09e0eb4 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -88,7 +88,7 @@ export class ItemPageComponent implements OnInit, OnDestroy { href: link.href, type: link.type, rel: link.rel - }) + }); }); this.responseService.setHeader('Link', links); @@ -117,6 +117,6 @@ export class ItemPageComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.signpostingLinks.forEach((link: SignpostingLink) => { this.linkHeadService.removeTag(`href='${link.href}'`); - }) + }); } } From 43d78c695c7e6f763a8235361cdade0048a77e5e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 15 May 2023 17:11:45 +0200 Subject: [PATCH 11/15] [CST-5729] Assign a default value --- src/app/item-page/simple/item-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index c1c09e0eb4..f5ee9e1e78 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -65,7 +65,7 @@ export class ItemPageComponent implements OnInit, OnDestroy { /** * Contains a list of SignpostingLink related to the item */ - signpostingLinks: SignpostingLink[]; + signpostingLinks: SignpostingLink[] = []; constructor( protected route: ActivatedRoute, From 220b30bea7661b1e7af58611bd3ff3531cdb0421 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 May 2023 23:15:57 +0200 Subject: [PATCH 12/15] [CST-5729] Address review feedback --- .../bitstream-download-page.component.spec.ts | 4 +- .../bitstream-download-page.component.ts | 40 +++++++++------ src/app/core/data/signposting-data.service.ts | 8 +++ .../core/services/server-response.service.ts | 36 +++++++++++++ src/app/init.service.ts | 1 - .../full/full-item-page.component.spec.ts | 3 +- .../full/full-item-page.component.ts | 7 +-- .../simple/item-page.component.spec.ts | 32 +++++++++--- .../item-page/simple/item-page.component.ts | 50 ++++++++++++------- 9 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index 66024063cd..59261e56d2 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -13,6 +13,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { ServerResponseService } from '../../core/services/server-response.service'; +import { PLATFORM_ID } from '@angular/core'; describe('BitstreamDownloadPageComponent', () => { let component: BitstreamDownloadPageComponent; @@ -99,7 +100,8 @@ describe('BitstreamDownloadPageComponent', () => { { provide: FileService, useValue: fileService }, { provide: HardRedirectService, useValue: hardRedirectService }, { provide: ServerResponseService, useValue: serverResponseService }, - { provide: SignpostingDataService, useValue: signpostingDataService } + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: PLATFORM_ID, useValue: 'server' } ] }) .compileComponents(); diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 14245c4cd1..0becfcf473 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -13,7 +13,7 @@ import { HardRedirectService } from '../../core/services/hard-redirect.service'; import { getForbiddenRoute } from '../../app-routing-paths'; import { RemoteData } from '../../core/data/remote-data'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; -import { Location } from '@angular/common'; +import { isPlatformServer, Location } from '@angular/common'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; import { ServerResponseService } from 'src/app/core/services/server-response.service'; import { SignpostingLink } from '../../core/data/signposting-links.model'; @@ -39,19 +39,10 @@ export class BitstreamDownloadPageComponent implements OnInit { private hardRedirectService: HardRedirectService, private location: Location, private signpostingDataService: SignpostingDataService, - private responseService: ServerResponseService + private responseService: ServerResponseService, + @Inject(PLATFORM_ID) protected platformId: string ) { - this.route.params.subscribe(params => { - this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { - let links = ''; - - signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - }); - - this.responseService.setHeader('Link', links); - }); - }); + this.initPageLinks(); } back(): void { @@ -101,4 +92,25 @@ export class BitstreamDownloadPageComponent implements OnInit { } }); } + + /** + * Create page links if any are retrieved by signposting endpoint + * + * @private + */ + private initPageLinks(): void { + if (isPlatformServer(this.platformId)) { + this.route.params.subscribe(params => { + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + }); + + this.responseService.setHeader('Link', links); + }); + }); + } + } } diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index e09d68974c..fca22ec383 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -8,6 +8,9 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { SignpostingLink } from './signposting-links.model'; +/** + * Service responsible for handling requests related to the Signposting endpoint + */ @Injectable({ providedIn: 'root' }) @@ -16,6 +19,11 @@ export class SignpostingDataService { constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + /** + * Retrieve the list of signposting links related to the given resource's id + * + * @param uuid + */ getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index 2268e9eb03..0b193d536c 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -1,7 +1,11 @@ import { RESPONSE } from '@nguniversal/express-engine/tokens'; import { Inject, Injectable, Optional } from '@angular/core'; + import { Response } from 'express'; +/** + * Service responsible to provide method to manage the response object + */ @Injectable() export class ServerResponseService { private response: Response; @@ -10,6 +14,12 @@ export class ServerResponseService { this.response = response; } + /** + * Set a status code to response + * + * @param code + * @param message + */ setStatus(code: number, message?: string): this { if (this.response) { this.response.statusCode = code; @@ -20,22 +30,48 @@ export class ServerResponseService { return this; } + /** + * Set Unauthorized status + * + * @param message + */ setUnauthorized(message = 'Unauthorized'): this { return this.setStatus(401, message); } + /** + * Set Forbidden status + * + * @param message + */ setForbidden(message = 'Forbidden'): this { return this.setStatus(403, message); } + /** + * Set Not found status + * + * @param message + */ setNotFound(message = 'Not found'): this { return this.setStatus(404, message); } + /** + * Set Internal Server Error status + * + * @param message + */ setInternalServerError(message = 'Internal Server Error'): this { return this.setStatus(500, message); } + /** + * Set a response's header + * + * @param header + * @param content + */ setHeader(header: string, content: string) { if (this.response) { this.response.setHeader(header, content); diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 2bbc589cc0..9fef2ca4ed 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -188,7 +188,6 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); this.menuService.listenForRouteChanges(); - // this.metadataItem.checkCurrentRoute(); } /** 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 ec4054d888..9fc078c2cd 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 @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/cor import { ItemDataService } from '../../core/data/item-data.service'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { TruncatePipe } from '../../shared/utils/truncate.pipe'; import { FullItemPageComponent } from './full-item-page.component'; import { MetadataService } from '../../core/metadata/metadata.service'; @@ -122,6 +122,7 @@ describe('FullItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { 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 f0100eed72..31dd2c5fc2 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -1,5 +1,5 @@ import { filter, map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Data, Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -54,9 +54,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, protected _location: Location, protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, - protected linkHeadService: LinkHeadService + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string, ) { - super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService); + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ 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 005142e3f1..dfba4bd235 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { ItemDataService } from '../../core/data/item-data.service'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { ItemPageComponent } from './item-page.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; @@ -24,7 +24,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; 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 { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -41,16 +42,18 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { const mocklink = { href: 'http://test.org', - rel: 'test', - type: 'test' + rel: 'rel1', + type: 'type1' }; const mocklink2 = { href: 'http://test2.org', - rel: 'test', - type: 'test' + rel: 'rel2', + type: 'type2' }; +const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2]; + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; @@ -109,6 +112,7 @@ describe('ItemPageComponent', () => { { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' }, ], schemas: [NO_ERRORS_SCHEMA] @@ -165,6 +169,22 @@ describe('ItemPageComponent', () => { expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); }); + + it('should add link tags correctly', () => { + + 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).toHaveBeenCalledWith(mockSignpostingLinks[0] as LinkDefinition); + expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[1] as LinkDefinition); + }); + + it('should set Link header on the server', () => { + + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" ; type="type2" '); + }); + }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index f5ee9e1e78..a11cb22883 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,5 +1,6 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { isPlatformServer } from '@angular/common'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -75,25 +76,10 @@ export class ItemPageComponent implements OnInit, OnDestroy { protected authorizationService: AuthorizationDataService, protected responseService: ServerResponseService, protected signpostingDataService: SignpostingDataService, - protected linkHeadService: LinkHeadService + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string ) { - this.route.params.subscribe(params => { - this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { - let links = ''; - this.signpostingLinks = signpostingLinks; - - signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - this.linkHeadService.addTag({ - href: link.href, - type: link.type, - rel: link.rel - }); - }); - - this.responseService.setHeader('Link', links); - }); - }); + this.initPageLinks(); } /** @@ -113,6 +99,32 @@ export class ItemPageComponent implements OnInit, OnDestroy { } + /** + * Create page links if any are retrieved by signposting endpoint + * + * @private + */ + private initPageLinks(): void { + this.route.params.subscribe(params => { + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + this.signpostingLinks = signpostingLinks; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; + this.linkHeadService.addTag({ + href: link.href, + type: link.type, + rel: link.rel + }); + }); + + if (isPlatformServer(this.platformId)) { + this.responseService.setHeader('Link', links); + } + }); + }); + } ngOnDestroy(): void { this.signpostingLinks.forEach((link: SignpostingLink) => { From 092608f6cbb114a81a34a685fefec8e5cbed2bed Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 5 Jun 2023 15:11:01 +0200 Subject: [PATCH 13/15] [CST-5729] Remove additional error message --- src/app/core/data/signposting-data.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index fca22ec383..d051ecf8db 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -29,7 +29,6 @@ export class SignpostingDataService { return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( catchError((err) => { - console.error(err); return observableOf([]); }), map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : []) From 02bb7db1190c54be1f2a00608c624fdf67804f23 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 8 Jun 2023 13:15:02 +0200 Subject: [PATCH 14/15] [CST-5729] Fix issue with undefined link type --- .../bitstream-download-page.component.ts | 1 + .../item-page/simple/item-page.component.spec.ts | 13 +++++++++---- src/app/item-page/simple/item-page.component.ts | 15 ++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 4d05511ca0..cf8d8e7767 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -107,6 +107,7 @@ export class BitstreamDownloadPageComponent implements OnInit { let links = ''; signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; }); 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 dfba4bd235..b3202108f4 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -49,7 +49,7 @@ const mocklink = { const mocklink2 = { href: 'http://test2.org', rel: 'rel2', - type: 'type2' + type: undefined }; const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2]; @@ -176,13 +176,18 @@ describe('ItemPageComponent', () => { // Check if linkHeadService.addTag() was called with the correct arguments expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length); - expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[0] as LinkDefinition); - expect(linkHeadService.addTag).toHaveBeenCalledWith(mockSignpostingLinks[1] as LinkDefinition); + let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); + expected = { + href: 'http://test2.org', + rel: 'rel2' + }; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); }); it('should set Link header on the server', () => { - expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" ; type="type2" '); + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" '); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index a11cb22883..b9be6bebfb 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -20,7 +20,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { SignpostingLink } from '../../core/data/signposting-links.model'; import { isNotEmpty } from '../../shared/empty.util'; -import { LinkHeadService } from '../../core/services/link-head.service'; +import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a simple item page. @@ -111,12 +111,17 @@ export class ItemPageComponent implements OnInit, OnDestroy { this.signpostingLinks = signpostingLinks; signpostingLinks.forEach((link: SignpostingLink) => { - links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `; - this.linkHeadService.addTag({ + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); + let tag: LinkDefinition = { href: link.href, - type: link.type, rel: link.rel - }); + }; + if (isNotEmpty(link.type)) { + tag = Object.assign(tag, { + type: link.type + }); + } + this.linkHeadService.addTag(tag); }); if (isPlatformServer(this.platformId)) { From 96903d89dedfc6191fc71b5b1b81253783459d74 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 9 Jun 2023 19:24:16 +0200 Subject: [PATCH 15/15] [CST-5729] fix signposting proxy url --- server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.ts b/server.ts index 282b1ce29a..d64b80b4ab 100644 --- a/server.ts +++ b/server.ts @@ -182,8 +182,8 @@ export function app() { /** * Proxy the linksets */ - router.use('/links**', createProxyMiddleware({ - target: `${environment.rest.baseUrl}/signposting`, + router.use('/signposting**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), changeOrigin: true }));