mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #2248 from 4Science/feature/CST-5729
Implement Signposting on angular side
This commit is contained in:
12
server.ts
12
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';
|
||||
@@ -180,6 +179,15 @@ export function app() {
|
||||
changeOrigin: true
|
||||
}));
|
||||
|
||||
/**
|
||||
* Proxy the linksets
|
||||
*/
|
||||
router.use('/signposting**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}`,
|
||||
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.
|
||||
|
@@ -11,6 +11,9 @@ 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';
|
||||
import { PLATFORM_ID } from '@angular/core';
|
||||
|
||||
describe('BitstreamDownloadPageComponent', () => {
|
||||
let component: BitstreamDownloadPageComponent;
|
||||
@@ -24,6 +27,20 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
let router;
|
||||
|
||||
let bitstream: Bitstream;
|
||||
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||
|
||||
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', {
|
||||
@@ -54,10 +71,21 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
bitstream: createSuccessfulRemoteDataObject(
|
||||
bitstream
|
||||
)
|
||||
}),
|
||||
params: observableOf({
|
||||
id: 'testid'
|
||||
})
|
||||
};
|
||||
|
||||
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
||||
|
||||
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||
setHeader: jasmine.createSpy('setHeader'),
|
||||
});
|
||||
|
||||
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||
getLinks: observableOf([mocklink, mocklink2])
|
||||
});
|
||||
}
|
||||
|
||||
function initTestbed() {
|
||||
@@ -71,6 +99,9 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: FileService, useValue: fileService },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||
{ provide: PLATFORM_ID, useValue: 'server' }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
@@ -107,6 +138,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(() => {
|
||||
|
@@ -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,8 +13,11 @@ 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 { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-bitstream-download-page',
|
||||
@@ -28,7 +31,6 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
||||
bitstream$: Observable<Bitstream>;
|
||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
@@ -38,8 +40,11 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private location: Location,
|
||||
public dsoNameService: DSONameService,
|
||||
private signpostingDataService: SignpostingDataService,
|
||||
private responseService: ServerResponseService,
|
||||
@Inject(PLATFORM_ID) protected platformId: string
|
||||
) {
|
||||
|
||||
this.initPageLinks();
|
||||
}
|
||||
|
||||
back(): void {
|
||||
@@ -89,4 +94,26 @@ 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}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' ');
|
||||
links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `;
|
||||
});
|
||||
|
||||
this.responseService.setHeader('Link', links);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
97
src/app/core/data/signposting-data.service.spec.ts
Normal file
97
src/app/core/data/signposting-data.service.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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 { SignpostingLink } from './signposting-links.model';
|
||||
|
||||
describe('SignpostingDataService', () => {
|
||||
let service: SignpostingDataService;
|
||||
let restServiceSpy: jasmine.SpyObj<DspaceRestService>;
|
||||
let halServiceSpy: jasmine.SpyObj<HALEndpointService>;
|
||||
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']);
|
||||
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<DspaceRestService>;
|
||||
halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj<HALEndpointService>;
|
||||
});
|
||||
|
||||
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`);
|
||||
|
||||
restServiceSpy.get.and.returnValue(of(mockResponse));
|
||||
|
||||
let result: SignpostingLink[];
|
||||
|
||||
const expectedResult: SignpostingLink[] = [mocklink, mocklink2];
|
||||
|
||||
service.getLinks(uuid).subscribe((links) => {
|
||||
result = links;
|
||||
});
|
||||
|
||||
tick();
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(halServiceSpy.getRootHref).toHaveBeenCalled();
|
||||
expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`);
|
||||
}));
|
||||
|
||||
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(mockErrResponse));
|
||||
|
||||
let result: any;
|
||||
|
||||
service.getLinks(uuid).subscribe((data) => {
|
||||
result = data;
|
||||
});
|
||||
|
||||
tick();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(halServiceSpy.getRootHref).toHaveBeenCalled();
|
||||
expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`);
|
||||
}));
|
||||
});
|
38
src/app/core/data/signposting-data.service.ts
Normal file
38
src/app/core/data/signposting-data.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
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 { SignpostingLink } from './signposting-links.model';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Signposting endpoint
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
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<SignpostingLink[]> {
|
||||
const baseUrl = this.halService.getRootHref().replace('/api', '');
|
||||
|
||||
return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe(
|
||||
catchError((err) => {
|
||||
return observableOf([]);
|
||||
}),
|
||||
map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : [])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
8
src/app/core/data/signposting-links.model.ts
Normal file
8
src/app/core/data/signposting-links.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Represents the link object received by the signposting endpoint
|
||||
*/
|
||||
export interface SignpostingLink {
|
||||
href?: string,
|
||||
rel?: string,
|
||||
type?: string
|
||||
}
|
@@ -46,6 +46,7 @@ export class ServerHardRedirectService extends HardRedirectService {
|
||||
}
|
||||
|
||||
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
||||
|
||||
this.res.redirect(status, url);
|
||||
this.res.end();
|
||||
// I haven't found a way to correctly stop Angular rendering.
|
||||
|
@@ -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,19 +30,51 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -20,6 +20,9 @@ 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';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
@@ -55,8 +58,21 @@ describe('FullItemPageComponent', () => {
|
||||
let routeStub: ActivatedRouteStub;
|
||||
let routeData;
|
||||
let authorizationDataService: AuthorizationDataService;
|
||||
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||
|
||||
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', {
|
||||
@@ -76,6 +92,19 @@ describe('FullItemPageComponent', () => {
|
||||
isAuthorized: observableOf(false),
|
||||
});
|
||||
|
||||
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||
setHeader: jasmine.createSpy('setHeader'),
|
||||
});
|
||||
|
||||
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||
getLinks: observableOf([mocklink, mocklink2]),
|
||||
});
|
||||
|
||||
linkHeadService = jasmine.createSpyObj('LinkHeadService', {
|
||||
addTag: jasmine.createSpy('setHeader'),
|
||||
removeTag: jasmine.createSpy('removeTag'),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -90,8 +119,11 @@ describe('FullItemPageComponent', () => {
|
||||
{ provide: MetadataService, useValue: metadataServiceStub },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||
{ provide: PLATFORM_ID, useValue: 'server' }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(FullItemPageComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
@@ -143,6 +175,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(() => {
|
||||
@@ -167,6 +204,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', () => {
|
||||
@@ -179,5 +221,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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';
|
||||
@@ -16,7 +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 '../../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.
|
||||
@@ -43,13 +45,19 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
|
||||
|
||||
subs = [];
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
router: Router,
|
||||
items: ItemDataService,
|
||||
authService: AuthService,
|
||||
authorizationService: AuthorizationDataService,
|
||||
private _location: Location) {
|
||||
super(route, router, items, authService, authorizationService);
|
||||
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,
|
||||
@Inject(PLATFORM_ID) protected platformId: string,
|
||||
) {
|
||||
super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId);
|
||||
}
|
||||
|
||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||
|
@@ -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';
|
||||
@@ -22,6 +22,10 @@ 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 { ServerResponseService } from '../../core/services/server-response.service';
|
||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
|
||||
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
@@ -36,11 +40,28 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), {
|
||||
isWithdrawn: true
|
||||
});
|
||||
|
||||
const mocklink = {
|
||||
href: 'http://test.org',
|
||||
rel: 'rel1',
|
||||
type: 'type1'
|
||||
};
|
||||
|
||||
const mocklink2 = {
|
||||
href: 'http://test2.org',
|
||||
rel: 'rel2',
|
||||
type: undefined
|
||||
};
|
||||
|
||||
const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2];
|
||||
|
||||
describe('ItemPageComponent', () => {
|
||||
let comp: ItemPageComponent;
|
||||
let fixture: ComponentFixture<ItemPageComponent>;
|
||||
let authService: AuthService;
|
||||
let authorizationDataService: AuthorizationDataService;
|
||||
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||
|
||||
const mockMetadataService = {
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
@@ -60,6 +81,18 @@ describe('ItemPageComponent', () => {
|
||||
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||
isAuthorized: observableOf(false),
|
||||
});
|
||||
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||
setHeader: jasmine.createSpy('setHeader'),
|
||||
});
|
||||
|
||||
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||
getLinks: observableOf([mocklink, mocklink2]),
|
||||
});
|
||||
|
||||
linkHeadService = jasmine.createSpyObj('LinkHeadService', {
|
||||
addTag: jasmine.createSpy('setHeader'),
|
||||
removeTag: jasmine.createSpy('removeTag'),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
@@ -76,6 +109,10 @@ describe('ItemPageComponent', () => {
|
||||
{ provide: Router, useValue: {} },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -126,6 +163,33 @@ 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);
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
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', '<http://test.org> ; rel="rel1" ; type="type1" , <http://test2.org> ; rel="rel2" ');
|
||||
});
|
||||
|
||||
});
|
||||
describe('when the item is withdrawn and the user is not an admin', () => {
|
||||
beforeEach(() => {
|
||||
@@ -150,6 +214,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', () => {
|
||||
@@ -162,6 +231,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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, 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 } from 'rxjs/operators';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
@@ -15,6 +16,11 @@ 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';
|
||||
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -28,7 +34,7 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [fadeInOut]
|
||||
})
|
||||
export class ItemPageComponent implements OnInit {
|
||||
export class ItemPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The item's id
|
||||
@@ -57,13 +63,23 @@ 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
|
||||
protected router: Router,
|
||||
protected items: ItemDataService,
|
||||
protected authService: AuthService,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected responseService: ServerResponseService,
|
||||
protected signpostingDataService: SignpostingDataService,
|
||||
protected linkHeadService: LinkHeadService,
|
||||
@Inject(PLATFORM_ID) protected platformId: string
|
||||
) {
|
||||
this.initPageLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,4 +98,42 @@ export class ItemPageComponent implements OnInit {
|
||||
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' ');
|
||||
let tag: LinkDefinition = {
|
||||
href: link.href,
|
||||
rel: link.rel
|
||||
};
|
||||
if (isNotEmpty(link.type)) {
|
||||
tag = Object.assign(tag, {
|
||||
type: link.type
|
||||
});
|
||||
}
|
||||
this.linkHeadService.addTag(tag);
|
||||
});
|
||||
|
||||
if (isPlatformServer(this.platformId)) {
|
||||
this.responseService.setHeader('Link', links);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.signpostingLinks.forEach((link: SignpostingLink) => {
|
||||
this.linkHeadService.removeTag(`href='${link.href}'`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ export class ServerInitService extends InitService {
|
||||
protected metadata: MetadataService,
|
||||
protected breadcrumbsService: BreadcrumbsService,
|
||||
protected themeService: ThemeService,
|
||||
protected menuService: MenuService,
|
||||
protected menuService: MenuService
|
||||
) {
|
||||
super(
|
||||
store,
|
||||
|
Reference in New Issue
Block a user