diff --git a/CHANGELOG.md b/CHANGELOG.md index 8864329fa6..99e597b04f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.7.1](https://github.com/webern-unibas-ch/awg-app/compare/v0.7.0...v0.7.1) (2020-02-10) + +### Features + +- **core:** add RouterEventsService to store previous route ([23e3656](https://github.com/webern-unibas-ch/awg-app/commit/23e365690b35b6ec9f7169401f1d9fbe35883db9)) +- **core:** add StorageService ([bde1f1e](https://github.com/webern-unibas-ch/awg-app/commit/bde1f1eff3b49341d1ebf621cc5f535b4e9ec1e4)), closes [#5](https://github.com/webern-unibas-ch/awg-app/issues/5) +- **core:** expose GND via postMessage to communicate with inseri ([80648c5](https://github.com/webern-unibas-ch/awg-app/commit/80648c5204a7bc0a46455491bff5de23497978f1)), relates to [nie-ine/inseri#388](https://github.com/nie-ine/inseri/issues/388) +- **edition:** add almost complete TkA for op25 (Sk4 incomplete) ([3dabd8a](https://github.com/webern-unibas-ch/awg-app/commit/3dabd8af6b438088b7a87c65b2134b9e39f58dbe)) +- **edition:** make tka list toggleable per sketch ([01ec0fd](https://github.com/webern-unibas-ch/awg-app/commit/01ec0fd45db4f92e6e01e8364045c6a6df4bacce)) +- **edition:** prepare embedding of op25 sheets ([1f25f63](https://github.com/webern-unibas-ch/awg-app/commit/1f25f63148e61f00461496d25081a448a6310091)) + +### Bug Fixes + +- **app:** move GND exposition to PropsComp and GndService ([e44b332](https://github.com/webern-unibas-ch/awg-app/commit/e44b3324d092d2b799242c9a7b0d0a3cfce371a6), [98bd896](https://github.com/webern-unibas-ch/awg-app/commit/98bd896d6151c09d806fef1c82734e899a55d43d)) +- **core:** add removeItem method to StorageService ([19a6f8d](https://github.com/webern-unibas-ch/awg-app/commit/19a6f8d85548782397e836cd8fd802fb1d840628)) +- **core:** fix check for detecting Storage ([b5c4c08](https://github.com/webern-unibas-ch/awg-app/commit/b5c4c08bf370e9bb6048fa9775ab30ea8690ce45)) +- **core:** use StorageService to expose GND ([e591885](https://github.com/webern-unibas-ch/awg-app/commit/e591885d64977ae9ef87ab6bdc58193f323adefd)) +- **edition:** add missing content description of op. 25 ([29d1c34](https://github.com/webern-unibas-ch/awg-app/commit/29d1c34b7e31b0135982a9061085e75b1d781b90)) +- **edition:** add svg's with path information ([50d23ba](https://github.com/webern-unibas-ch/awg-app/commit/50d23ba0e6413f89f7f759ef8f023ea1296cbc94)) +- **edition:** adjust modal hint for op25 ([d6b4909](https://github.com/webern-unibas-ch/awg-app/commit/d6b4909428450716f320c568763feb7f19cdf4f9)) +- **edition:** get selectability of convolute item from data ([9fcb2dd](https://github.com/webern-unibas-ch/awg-app/commit/9fcb2ddff22f49a019c41c71fcf2d488dcd3fd75)) +- **edition:** handle placeholder for op. 12 Aa:SkI/1 ([ecbb32b](https://github.com/webern-unibas-ch/awg-app/commit/ecbb32b228438a19768ba8b83ef85ad07d7c90d0)) +- **edition:** improve folio handling and rendering ([8c871cc](https://github.com/webern-unibas-ch/awg-app/commit/8c871cc7f435910daea3789a0c2b111982904a29)) +- **edition:** move convolute logic to parent component (edition detail) ([7a9c5ed](https://github.com/webern-unibas-ch/awg-app/commit/7a9c5edaca439fe54a887de7afd75a42496e4dc6), [fb77f72](https://github.com/webern-unibas-ch/awg-app/commit/fb77f72aa9c817c8e0d09e2e9d204dfbf8508683)) +- **home:** adjust title of op. 25 ([9f1c9fc](https://github.com/webern-unibas-ch/awg-app/commit/9f1c9fc07960335d2e4c5c8bec0f7ace44054a9f)) + ## [0.7.0](https://github.com/webern-unibas-ch/awg-app/compare/v0.6.1...v0.7.0) (2020-02-05) ### ⚠ BREAKING CHANGES diff --git a/package.json b/package.json index 321a2a755f..743efe9027 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "awg-app", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "author": { "name": "Stefan Münnich", @@ -27,14 +27,16 @@ "start": "ng serve", "e2e": "ng e2e", "test": "ng test", - "test:cov": "ng test awg-app --code-coverage", + "test:cov": "ng test --code-coverage true", + "serve:test:cov": "npx http-server -c-1 -o -p 9875 ./coverage", "test:ci": "yarn test:cov --watch=false --browsers=ChromeHeadlessNoSandbox", "lint": "ng lint awg-app", "tslint-check": "tslint-config-prettier-check ./tslint.json", "format:check": "prettier --check \"src/**/*.ts\"", "format:fix": "pretty-quick --staged", - "serve:doc": "yarn compodoc -p tsconfig.app.json --theme Readthedocs -d dist/compodoc -s -w", - "build:doc": "yarn compodoc -p tsconfig.app.json --theme Readthedocs -d dist/compodoc", + "doc": "yarn compodoc -p tsconfig.app.json --theme Readthedocs -d dist/compodoc", + "serve:doc": "yarn doc -s -w", + "build:doc": "yarn doc", "build": "ng build", "build:prod": "yarn build --prod --aot=false", "postbuild:prod": "run-s gzip:dist build:doc", diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 83969060d6..4bf75ab143 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -5,8 +5,11 @@ import { NavigationEnd, Router } from '@angular/router'; import { of as observableOf } from 'rxjs'; +import Spy = jasmine.Spy; + import { getAndExpectDebugElementByDirective } from '@testing/expect-helper'; +import { RouterEventsService } from '@awg-core/services'; import { AppComponent } from './app.component'; // analytics global @@ -22,7 +25,7 @@ class ViewContainerStubComponent {} @Component({ selector: 'awg-footer', template: '' }) class FooterStubComponent {} -class MockServices { +class MockRouter { // Router events = observableOf(new NavigationEnd(0, 'testUrl', 'testUrl'), [0, 0], 'testString'); } @@ -35,10 +38,20 @@ describe('AppComponent (DONE)', () => { let router: Router; + let getPreviousRouteSpy: Spy; + beforeEach(async(() => { + // create a fake RouterEventsService object with a `getPreviousRoute` spy + const mockRouterEventsService = jasmine.createSpyObj('RouterEventsService', ['getPreviousRoute']); + // make the spies return a synchronous Observable with the test data + getPreviousRouteSpy = mockRouterEventsService.getPreviousRoute.and.returnValue(observableOf('')); + TestBed.configureTestingModule({ declarations: [AppComponent, FooterStubComponent, NavbarStubComponent, ViewContainerStubComponent], - providers: [{ provide: Router, useClass: MockServices }] + providers: [ + { provide: Router, useClass: MockRouter }, + { provide: RouterEventsService, useValue: mockRouterEventsService } + ] }).compileComponents(); })); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4ae00b921d..81967fe553 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,8 @@ import { Component } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; +import { RouterEventsService } from '@awg-core/services'; + /** * The main component of the AWG App. * @@ -15,14 +17,16 @@ export class AppComponent { /** * Constructor of the AppComponent. * - * It declares a private router instance to catch GoogleAnalytics pageview events, - * see {@link https://codeburst.io/using-google-analytics-with-angular-25c93bffaa18}. + * It declares private instances of the Angular router and the RouterEventsService. * * @param {Router} router Instance of the Angular router. + * @param {RouterEventsService} routerEventsService Instance of the RouterEventsService. */ - constructor(private readonly router: Router) { + constructor(private readonly router: Router, private routerEventsService: RouterEventsService) { this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { + // catch GoogleAnalytics pageview events, + // cf. https://codeburst.io/using-google-analytics-with-angular-25c93bffaa18 (window as any).ga('set', 'page', event.urlAfterRedirects); (window as any).ga('send', 'pageview'); } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 4fced24519..44c491f4d5 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -16,6 +16,15 @@ export class AppConfig { return root + api; } + /** + * Getter for the AWG route storage key. + * + * @returns {string} + */ + public static get AWG_APP_ROUTE_STORAGE_KEY(): string { + return 'awg-app-route'; + } + /** * Getter for the url of the AWG edition website (awg-app). * diff --git a/src/app/app.globals.ts b/src/app/app.globals.ts index 2b258c7079..f1290cc460 100644 --- a/src/app/app.globals.ts +++ b/src/app/app.globals.ts @@ -1,15 +1,15 @@ // THIS IS AN AUTO-GENERATED FILE. DO NOT CHANGE IT MANUALLY! -// Generated last time on Wed Feb 5 14:49:11 CET 2020 +// Generated last time on Mon Feb 10 12:21:25 2020 /** * The latest version of the AWG App */ -export const appVersion = '0.7.0'; +export const appVersion = '0.7.1'; /** * The release date of the latest version of the AWG App */ -export const appVersionReleaseDate = '05. Februar 2020'; +export const appVersionReleaseDate = '10. Februar 2020'; /** * The URL of the AWG App diff --git a/src/app/core/services/api-service/api.service.ts b/src/app/core/services/api-service/api.service.ts index f9de2597f7..2ac57d93c6 100644 --- a/src/app/core/services/api-service/api.service.ts +++ b/src/app/core/services/api-service/api.service.ts @@ -37,7 +37,7 @@ export class ApiService { /** * Constructor of the ApiService. * - * It declares a private {@link HttpClient} instance + * It declares a public {@link HttpClient} instance * to handle http requests. * * @param {HttpClient} http Instance of the HttpClient. diff --git a/src/app/core/services/conversion-service/conversion.service.ts b/src/app/core/services/conversion-service/conversion.service.ts index 60e8e68908..4107a7543f 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -1,10 +1,12 @@ import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { NgxGalleryImage } from '@kolkov/ngx-gallery'; import { ApiService } from '@awg-core/services/api-service'; + import { GeoNames } from '@awg-core/core-models'; import { ContextJson, @@ -21,7 +23,6 @@ import { SelectionJson, SubjectItemJson } from '@awg-shared/api-objects'; -import { PropertyJsonValue } from '@awg-shared/api-objects/resource-response-formats/src/property-json'; import { IResourceDataResponse, ResourceDetail, @@ -69,6 +70,19 @@ export class ConversionService extends ApiService { */ filteredOut: number; + /** + * Constructor of the ConversionService. + * + * It declares a public {@link HttpClient} instance + * with a super reference to base class (ApiService) + * and a private {@link StorageService} instance. + * + * @param {HttpClient} http Instance of the HttpClient. + */ + constructor(public http: HttpClient) { + super(http); + } + /** * Public method: convertFullTextSearchResults. * @@ -494,10 +508,6 @@ export class ConversionService extends ApiService { case '14': // RICHTEXT: salsah standoff needs to be converted for (let i = 0; i < prop.values.length; i++) { - // if we have a gnd (prop.pid=856), write it to localstorage - if (prop.pid === '856' && prop.values[i]) { - this.writeGndToLocalStorage(prop.values[i]); - } // convert richtext standoff prop.toHtml[i] = this.convertRichtextValue(prop.values[i].utf8str, prop.values[i].textattr); } @@ -950,32 +960,4 @@ export class ConversionService extends ApiService { links: incomingLinks.filter(incomingLink => incomingLink.restype.label === label) })); } - - /** - * Private method: writeGndToLocalStorage. - * - * It writes the GND number to the localStorage - * for further processing. - * - * @param {} value The given incoming property value. - * - * @returns {void} Writes the GND number to the localStorage. - */ - private writeGndToLocalStorage(value: PropertyJsonValue): void { - const dnbReg = 'http://d-nb.info/gnd/'; - const gndKey = 'gnd'; - let gndItem: string; - - const valueHasGnd = (checkValue: PropertyJsonValue) => { - return !(!checkValue || !checkValue.utf8str || !checkValue.utf8str.includes(dnbReg)); - }; - - if (valueHasGnd(value)) { - // split utf8str with gnd value into array and take last argument (pop) - gndItem = value.utf8str.split(dnbReg).pop(); - localStorage.setItem(gndKey, gndItem); - } else { - localStorage.removeItem(gndKey); - } - } } diff --git a/src/app/core/services/core-service/core.service.spec.ts b/src/app/core/services/core-service/core.service.spec.ts index 7af2e5f7a3..71abf8b5b6 100644 --- a/src/app/core/services/core-service/core.service.spec.ts +++ b/src/app/core/services/core-service/core.service.spec.ts @@ -15,7 +15,7 @@ describe('CoreService (DONE)', () => { TestBed.configureTestingModule({ providers: [CoreService] }); - // inject services and http client handler + // inject service coreService = TestBed.get(CoreService); // test data diff --git a/src/app/core/services/data-streamer/data-streamer.service.spec.ts b/src/app/core/services/data-streamer-service/data-streamer.service.spec.ts similarity index 100% rename from src/app/core/services/data-streamer/data-streamer.service.spec.ts rename to src/app/core/services/data-streamer-service/data-streamer.service.spec.ts diff --git a/src/app/core/services/data-streamer/data-streamer.service.ts b/src/app/core/services/data-streamer-service/data-streamer.service.ts similarity index 100% rename from src/app/core/services/data-streamer/data-streamer.service.ts rename to src/app/core/services/data-streamer-service/data-streamer.service.ts diff --git a/src/app/core/services/data-streamer/index.ts b/src/app/core/services/data-streamer-service/index.ts similarity index 100% rename from src/app/core/services/data-streamer/index.ts rename to src/app/core/services/data-streamer-service/index.ts diff --git a/src/app/core/services/gnd-service/gnd.service.spec.ts b/src/app/core/services/gnd-service/gnd.service.spec.ts new file mode 100644 index 0000000000..176d1cc641 --- /dev/null +++ b/src/app/core/services/gnd-service/gnd.service.spec.ts @@ -0,0 +1,272 @@ +import { TestBed } from '@angular/core/testing'; + +import { expectSpyCall } from '@testing/expect-helper'; + +import { StorageType } from '@awg-core/services/storage-service'; +import { GndService } from './gnd.service'; + +describe('GndService', () => { + let gndService: GndService; + + const sessionType = StorageType.sessionStorage; + const localType = StorageType.localStorage; + + const expectedGndKey = 'gnd'; + const expectedDnbReg = /href="(https?:\/\/d-nb.info\/gnd\/([\w\-]{8,11}))"/i; + + let expectedMockStorage: Storage; + const expectedLocalStorage: Storage = window[localType]; + const expectedSessionStorage: Storage = window[sessionType]; + + const expectedInputValue = 'http://d-nb.info/gnd/12345678-X'; + const expectedItem = '12345678-X'; + const otherInputValue = 'http://d-nb.info/gnd/12345678-X'; + const otherItem = '87654321-A'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [GndService] + }); + // inject service + gndService = TestBed.get(GndService); + + // default to sessionStorage + expectedMockStorage = expectedSessionStorage; + + // mock Storage + let store = {}; + const mockStorage = { + getItem: (key: string): string => { + return key in store ? store[key] : null; + }, + setItem: (key: string, value: string) => { + store[key] = `${value}`; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + } + }; + + // spies replace storage calls with fake mockStorage calls + spyOn(localStorage, 'getItem').and.callFake(mockStorage.getItem); + spyOn(localStorage, 'setItem').and.callFake(mockStorage.setItem); + spyOn(localStorage, 'removeItem').and.callFake(mockStorage.removeItem); + spyOn(localStorage, 'clear').and.callFake(mockStorage.clear); + }); + + afterEach(() => { + // clear storages after each test + expectedSessionStorage.clear(); + expectedLocalStorage.clear(); + }); + + it('should be created', () => { + expect(gndService).toBeTruthy(); + }); + + it('should have gndKey', () => { + expect(gndService.gndKey).toBeTruthy(); + expect(gndService.gndKey).toEqual(expectedGndKey); + }); + + it('should have dnbReg', () => { + expect(gndService.dnbReg).toBeTruthy(); + expect(gndService.dnbReg).toEqual(expectedDnbReg); + }); + + it('should not have linkRegArr before setGndToSessionStorage call', () => { + expect(gndService.linkRegArr).toBeUndefined(); + + gndService.setGndToSessionStorage(expectedInputValue); + + expect(gndService.linkRegArr).toBeDefined(); + }); + + describe('#setGndToSessionStorage', () => { + it('... should set key/value pair to storage if value has gnd link', () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + gndService.setGndToSessionStorage(expectedInputValue); + + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`... should set an item to the correct storage if value has gnd link`, () => { + const otherStorage = expectedLocalStorage; + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(otherStorage.getItem(expectedGndKey)).toBeNull(); + + gndService.setGndToSessionStorage(expectedInputValue); + + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + expect(otherStorage.getItem(expectedGndKey)).not.toEqual(expectedItem, `should not be ${otherItem}`); + expect(otherStorage.getItem(expectedGndKey)).toBeNull(); + }); + + it('... should overwrite an existing gnd key if value has gnd link', () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + gndService.setGndToSessionStorage(expectedInputValue); + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + gndService.setGndToSessionStorage(otherInputValue); + expect(expectedMockStorage.getItem(expectedGndKey)).not.toEqual( + expectedItem, + `should not be ${expectedItem}` + ); + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(otherItem, `should be ${otherItem}`); + }); + + it('... should return null if value has no gnd link', () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + gndService.setGndToSessionStorage(expectedItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + }); + + it('... should call helper function with input value to check if value has gnd link', () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + const valueHasGndSpy = spyOn(gndService, 'valueHasGnd').and.callThrough(); + gndService.setGndToSessionStorage(expectedInputValue); + + expectSpyCall(valueHasGndSpy, 1, expectedInputValue); + }); + + describe('#valueHasGnd', () => { + it('... should execute regex check and populate linkRegArr if value has gnd link', () => { + expect(gndService.linkRegArr).toBeUndefined(); + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + const valueHasGndSpy = spyOn(gndService, 'valueHasGnd').and.callFake(checkValue => { + gndService.linkRegArr = gndService.dnbReg.exec(checkValue); + }); + gndService.setGndToSessionStorage(expectedInputValue); + + expectSpyCall(valueHasGndSpy, 1, expectedInputValue); + + expect(expectedInputValue).toMatch(expectedDnbReg); + expect(gndService.linkRegArr).toBeDefined(); + expect(gndService.linkRegArr).toEqual(expectedDnbReg.exec(expectedInputValue)); + }); + + it('... should execute regex check and set linkRegArr = null if value has no gnd link', () => { + expect(gndService.linkRegArr).toBeUndefined(); + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + const valueHasGndSpy = spyOn(gndService, 'valueHasGnd').and.callThrough(); + gndService.setGndToSessionStorage(otherItem); + + expectSpyCall(valueHasGndSpy, 1, otherItem); + + expect(otherItem).not.toMatch(expectedDnbReg); + expect(gndService.linkRegArr).toBeNull(); + }); + + it('... should return true if value has gnd link', () => { + expect(gndService.linkRegArr).toBeUndefined(); + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + const valueHasGndSpy = spyOn(gndService, 'valueHasGnd').and.callThrough(); + gndService.setGndToSessionStorage(expectedInputValue); + + expectSpyCall(valueHasGndSpy, 1, expectedInputValue); + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it('... should return false if value has no gnd link', () => { + expect(gndService.linkRegArr).toBeUndefined(); + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + const valueHasGndSpy = spyOn(gndService, 'valueHasGnd').and.callThrough(); + gndService.setGndToSessionStorage(otherItem); + + expectSpyCall(valueHasGndSpy, 1, otherItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + }); + }); + }); + + describe('removeGndFromSessionStorage', () => { + it(`... should remove an item by key from the storage`, () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expectedMockStorage.setItem(expectedGndKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + gndService.removeGndFromSessionStorage(); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + }); + + it(`... should remove an item from the correct storage`, () => { + const otherStorage = expectedLocalStorage; + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(otherStorage.getItem(expectedGndKey)).toBeNull(); + + expectedMockStorage.setItem(expectedGndKey, expectedItem); + otherStorage.setItem(expectedGndKey, otherItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + expect(otherStorage.getItem(expectedGndKey)).toEqual(otherItem, `should be ${otherItem}`); + + gndService.removeGndFromSessionStorage(); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(otherStorage.getItem(expectedGndKey)).toEqual(otherItem, `should be ${otherItem}`); + }); + + it(`... should remove the correct item from the storage`, () => { + const otherKey = 'otherKey'; + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(expectedMockStorage.getItem(otherKey)).toBeNull(); + + expectedMockStorage.setItem(expectedGndKey, expectedItem); + expectedMockStorage.setItem(otherKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toEqual(expectedItem, `should be ${expectedItem}`); + expect(expectedMockStorage.getItem(otherKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + gndService.removeGndFromSessionStorage(); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(expectedMockStorage.getItem(otherKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + describe(`... should do nothing if:`, () => { + it('- storage has not the gnd key', () => { + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + + gndService.removeGndFromSessionStorage(); + + expect(gndService.removeGndFromSessionStorage()).toBeUndefined(); + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + }); + + it(`- storage has other key but not the gnd key`, () => { + const otherKey = 'otherKey'; + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(expectedMockStorage.getItem(otherKey)).toBeNull(); + + expectedMockStorage.setItem(otherKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(expectedMockStorage.getItem(otherKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + gndService.removeGndFromSessionStorage(); + + expect(expectedMockStorage.getItem(expectedGndKey)).toBeNull(); + expect(expectedMockStorage.getItem(otherKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + }); + }); +}); diff --git a/src/app/core/services/gnd-service/gnd.service.ts b/src/app/core/services/gnd-service/gnd.service.ts new file mode 100644 index 0000000000..cff874330d --- /dev/null +++ b/src/app/core/services/gnd-service/gnd.service.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@angular/core'; + +import { StorageType, StorageService } from '@awg-core/services/storage-service'; + +/** + * The GndEventType enumeration. + * + * It stores the possible GND event types. + */ +export enum GndEventType { + set = 'set', + get = 'get', + remove = 'remove' +} + +/** + * The GndEvent class. + * + * It stores a GND event. + */ +export class GndEvent { + /** + * The type of the GND event. + */ + type: GndEventType; + + /** + * The value of the GND event (GND number). + */ + value: string; + + /** + * Constructor of the GndEvent class. + * + * It initializes the class with a given type and value. + * + * @param {GndEventType} type The given GND event type. + * @param {string} value The given GND value. + */ + constructor(type: GndEventType, value: string) { + this.type = type; + this.value = value; + } +} + +/** + * The GND service. + * + * It handles the exposure of GND values + * to the session storage via the StorageService + * + * Provided in: `root`. + */ +@Injectable({ + providedIn: 'root' +}) +export class GndService extends StorageService { + /** + * Readonly variable: gndKey. + * + * It holds the public key that is set to the storage. + */ + readonly gndKey = 'gnd'; + + /** + * Readonly variable: dnbReg. + * + * It holds the regular expression for a d-nb link in a href. + */ + readonly dnbReg = /href="(https?:\/\/d-nb.info\/gnd\/([\w\-]{8,11}))"/i; // regexp for d-nb links + + /** + * Public variable: linkRegArr. + * + * It holds the result array of a regex check execution . + */ + linkRegArr: RegExpExecArray; + + /** + * Public method: setGndToSessionStorage. + * + * It sets a given value to the key defined in 'gndKey' + * in the sessionStorage. + * + * @param {string} value The given input value. + * + * @returns {void} It sets the key/value pair to the storage. + */ + setGndToSessionStorage(value: string): void { + if (this.valueHasGnd(value)) { + let gndItem: string; + // take last argument (pop) of linkRegArray + gndItem = this.linkRegArr.pop().toString(); + + // set to storage + this.setStorageKey(StorageType.sessionStorage, this.gndKey, gndItem); + + // postMessage to communicate with Inseri + window.parent.window.postMessage({ for: 'user', key: gndItem }, 'http://localhost:4200'); + } else { + this.removeGndFromSessionStorage(); + } + } + + /** + * Public method: removeGndFromSessionStorage. + * + * It removes the key defined in 'gndKey' + * from the sessionStorage. + * + * @returns {void} It removes the key/value pair from the storage. + */ + removeGndFromSessionStorage(): void { + this.removeStorageKey(StorageType.sessionStorage, this.gndKey); + } + + /** + * Private method: valueHasGnd. + * + * It checks if a given value contains a GND link + * (checked via the dnbReg regex). + * + * @param {string} checkValue The given value to check. + * + * @return {boolean} The boolean result of the check. + */ + private valueHasGnd(checkValue: string): boolean { + if (this.dnbReg.test(checkValue)) { + this.linkRegArr = this.dnbReg.exec(checkValue); + } else { + this.linkRegArr = null; + } + return !!this.linkRegArr; + } +} diff --git a/src/app/core/services/gnd-service/index.ts b/src/app/core/services/gnd-service/index.ts new file mode 100644 index 0000000000..3f6c606015 --- /dev/null +++ b/src/app/core/services/gnd-service/index.ts @@ -0,0 +1 @@ +export * from './gnd.service'; diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index c9b751bdc6..50067ab69f 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -10,8 +10,21 @@ import { ApiService } from './api-service'; import { ConversionService } from './conversion-service'; import { CoreService } from './core-service'; -import { DataStreamerService } from './data-streamer'; +import { DataStreamerService } from './data-streamer-service'; +import { GndService } from './gnd-service'; import { LoadingService } from './loading-service'; +import { RouterEventsService } from './router-events-service'; import { SideInfoService } from './side-info-service'; +import { StorageService } from './storage-service'; -export { ApiService, ConversionService, CoreService, DataStreamerService, LoadingService, SideInfoService }; +export { + ApiService, + ConversionService, + CoreService, + DataStreamerService, + GndService, + LoadingService, + RouterEventsService, + SideInfoService, + StorageService +}; diff --git a/src/app/core/services/router-events-service/index.ts b/src/app/core/services/router-events-service/index.ts new file mode 100644 index 0000000000..61bc4d5af8 --- /dev/null +++ b/src/app/core/services/router-events-service/index.ts @@ -0,0 +1 @@ +export * from './router-events.service'; diff --git a/src/app/core/services/router-events-service/router-events.service.spec.ts b/src/app/core/services/router-events-service/router-events.service.spec.ts new file mode 100644 index 0000000000..9b240e21da --- /dev/null +++ b/src/app/core/services/router-events-service/router-events.service.spec.ts @@ -0,0 +1,22 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { RouterEventsService } from './router-events.service'; + +describe('RouterEventsService', () => { + let routerEventsService: RouterEventsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + providers: [RouterEventsService] + }); + + // inject service + routerEventsService = TestBed.get(RouterEventsService); + }); + + it('should be created', () => { + expect(routerEventsService).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/router-events-service/router-events.service.ts b/src/app/core/services/router-events-service/router-events.service.ts new file mode 100644 index 0000000000..575270dda9 --- /dev/null +++ b/src/app/core/services/router-events-service/router-events.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router, RoutesRecognized } from '@angular/router'; + +import { BehaviorSubject, Observable } from 'rxjs'; +import { tap, filter, pairwise } from 'rxjs/operators'; + +/** + * The RouterEvents service. + * + * It saves the previous route of a navigation event. + * cf. https://stackoverflow.com/a/59046339 + * + * Provided in: `root`. + */ +@Injectable({ + providedIn: 'root' +}) +export class RouterEventsService { + /** + * Private behavior subject to handle previous route path. + */ + private previousRoutePathSubject = new BehaviorSubject(''); + + /** + * Private readonly previousRoutePath stream as observable (`BehaviorSubject`). + */ + private readonly previousRoutePath$ = this.previousRoutePathSubject.asObservable(); + + /** + * Constructor of the RouterEventsService. + * + * It declares private instances of the Angular router and the Location. + * It filters the router events fo recognized routes + * to get the previous path from routing and stores it in the previousRoutePath. + * + * @param {Router} router Instance of the Angular router. + * @param {Location} locatoin Instance of the Location. + */ + constructor(private router: Router, private locatoin: Location) { + // ..initial previous route will be the current path for now + this.previousRoutePathSubject.next(this.locatoin.path()); + + // On every route change take the two events of two routes changed (using pairwise operator) + // and save the old one in a behaviour subject to access it in another component. + // Can be used if another component needs the previous route + // because it needs to redirect the user to where he did came from. + this.router.events + .pipe( + filter(e => e instanceof RoutesRecognized), + pairwise() + ) + .subscribe((event: any[]) => { + this.previousRoutePathSubject.next(event[0].urlAfterRedirects); + }); + } + + /** + * Public method: getPreviousRoute. + * + * It provides the previous route path from the previousRoutePath stream. + * + * @returns {Observable} The previousRoutePath stream as observable. + */ + getPreviousRoute(): Observable { + return this.previousRoutePath$; + } +} diff --git a/src/app/core/services/storage-service/index.ts b/src/app/core/services/storage-service/index.ts new file mode 100644 index 0000000000..4047195dc5 --- /dev/null +++ b/src/app/core/services/storage-service/index.ts @@ -0,0 +1 @@ +export * from './storage.service'; diff --git a/src/app/core/services/storage-service/storage.service.spec.ts b/src/app/core/services/storage-service/storage.service.spec.ts new file mode 100644 index 0000000000..ea75a130e1 --- /dev/null +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -0,0 +1,366 @@ +import { TestBed } from '@angular/core/testing'; + +import { StorageService, StorageType } from './storage.service'; + +describe('StorageService', () => { + let storageService: StorageService; + + const sessionType = StorageType.sessionStorage; + const localType = StorageType.localStorage; + + let expectedMockStorage: Storage; + const expectedLocalStorage: Storage = window[localType]; + const expectedSessionStorage: Storage = window[sessionType]; + + const expectedKey = 'key'; + const expectedItem = 'expectedItem'; + const otherItem = 'otherItem'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [StorageService] + }); + // inject service + storageService = TestBed.get(StorageService); + + // default to sessionStorage + expectedMockStorage = expectedSessionStorage; + + // mock Storage + let store = {}; + const mockStorage = { + getItem: (key: string): string => { + return key in store ? store[key] : null; + }, + setItem: (key: string, value: string) => { + store[key] = `${value}`; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + } + }; + + // spies replace storage calls with fake mockStorage calls + spyOn(localStorage, 'getItem').and.callFake(mockStorage.getItem); + spyOn(localStorage, 'setItem').and.callFake(mockStorage.setItem); + spyOn(localStorage, 'removeItem').and.callFake(mockStorage.removeItem); + spyOn(localStorage, 'clear').and.callFake(mockStorage.clear); + }); + + afterEach(() => { + // clear storages after each test + expectedSessionStorage.clear(); + expectedLocalStorage.clear(); + }); + + it('should be created', () => { + expect(storageService).toBeTruthy(); + }); + + describe('mockStorage', () => { + it('... should set, get and remove items from mockSessionStorage', () => { + expectedMockStorage = expectedSessionStorage; + + expect(expectedMockStorage.setItem('foo', 'bar')); + expect(expectedMockStorage.getItem('foo')).toBe('bar'); // bar + expect(expectedMockStorage.removeItem('foo')).toBeUndefined(); // undefined + expect(expectedMockStorage.getItem('foo')).toBeNull(); // null + }); + + it('... should set, get and remove items from mockLocalStorage', () => { + expectedMockStorage = expectedLocalStorage; + + expect(expectedMockStorage.setItem('foo', 'bar')); + expect(expectedMockStorage.getItem('foo')).toBe('bar'); // bar + expect(expectedMockStorage.removeItem('foo')).toBeUndefined(); // undefined + expect(expectedMockStorage.getItem('foo')).toBeNull(); // null + }); + + it('... should set, get items and clear mockSessionStorage', () => { + expectedMockStorage = expectedSessionStorage; + + expect(expectedMockStorage.setItem('foo', 'bar')); + expect(expectedMockStorage.setItem('bar', 'foo')); + expect(expectedMockStorage.getItem('foo')).toBe('bar'); // bar + expect(expectedMockStorage.getItem('bar')).toBe('foo'); // foo + expect(expectedMockStorage.clear()).toBeUndefined(); // undefined + expect(expectedMockStorage.getItem('foo')).toBeNull(); // null + expect(expectedMockStorage.getItem('bar')).toBeNull(); // null + }); + + it('... should set, get items and clear mockLocalStorage', () => { + expectedMockStorage = expectedLocalStorage; + + expect(expectedMockStorage.setItem('foo', 'bar')); + expect(expectedMockStorage.setItem('bar', 'foo')); + expect(expectedMockStorage.getItem('foo')).toBe('bar'); // bar + expect(expectedMockStorage.getItem('bar')).toBe('foo'); // foo + expect(expectedMockStorage.clear()).toBeUndefined(); // undefined + expect(expectedMockStorage.getItem('foo')).toBeNull(); // null + expect(expectedMockStorage.getItem('bar')).toBeNull(); // null + }); + }); + + describe('#setStorageKey', () => { + it(`... should set a given key/item string pair to a given storage type`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBe(expectedItem, `should be ${expectedItem}`); + }); + + it(`... should set item to the correct storage type`, () => { + const otherStorage = expectedLocalStorage; + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(otherStorage.getItem(expectedKey)).toBeNull(); + + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + storageService.setStorageKey(localType, expectedKey, otherItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + expect(otherStorage.getItem(expectedKey)).toEqual(otherItem, `should be ${otherItem}`); + }); + + it(`... should set a new key/item when a key does not exist`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`... should overwrite an existing item with the correct item when a key exists`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + storageService.setStorageKey(sessionType, expectedKey, otherItem); + expect(expectedMockStorage.getItem(expectedKey)).toEqual(otherItem, `should be ${otherItem}`); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(undefined, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage type is null`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(null, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage is not available`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + spyOn(storageService, 'storageIsAvailable').and.returnValue(false); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage is not supported`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + spyOn(storageService, 'storageIsSupported').and.returnValue(false); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- key is undefined `, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, undefined, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- key is null`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, null, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- value is undefined `, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, undefined); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- value is null`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, null); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + }); + }); + + describe('#getStorageKey', () => { + it(`... should get an item by key from a given storage type`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toEqual( + expectedItem, + `should be ${expectedItem}` + ); + }); + + it(`... should get item from the correct storage type`, () => { + const otherStorage = expectedLocalStorage; + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(otherStorage.getItem(expectedKey)).toBeNull(); + + expectedMockStorage.setItem(expectedKey, expectedItem); + otherStorage.setItem(expectedKey, otherItem); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toEqual( + expectedItem, + `should be ${expectedItem}` + ); + expect(storageService.getStorageKey(localType, expectedKey)).toEqual(otherItem, `should be ${otherItem}`); + }); + + it('... should return null for non existing keys', () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(storageService.getStorageKey(undefined, expectedKey)).toBeNull(); + }); + + it(`- storage type is null`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(storageService.getStorageKey(null, expectedKey)).toBeNull(); + }); + + it(`- storage has not the given key`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + spyOn(storageService, 'storageHasKey').and.returnValue(false); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`- storage is not supported`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + spyOn(storageService, 'storageIsSupported').and.returnValue(undefined); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`- storage is not available`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + spyOn(storageService, 'storageIsAvailable').and.returnValue(undefined); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + }); + }); + + describe('#removeStorageKey', () => { + it(`... should remove an item by key from a given storage type`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBe(expectedItem, `should be ${expectedItem}`); + + storageService.removeStorageKey(sessionType, expectedKey); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`... should remove item from the correct storage type`, () => { + const otherStorage = expectedLocalStorage; + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(otherStorage.getItem(expectedKey)).toBeNull(); + + expectedMockStorage.setItem(expectedKey, expectedItem); + otherStorage.setItem(expectedKey, otherItem); + + storageService.removeStorageKey(sessionType, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(otherStorage.getItem(expectedKey)).toEqual(otherItem, `should be ${otherItem}`); + + storageService.removeStorageKey(localType, expectedKey); + + expect(otherStorage.getItem(expectedKey)).toBeNull(); + }); + + it('... should return for non existing items', () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expect(storageService.removeStorageKey(sessionType, expectedKey)).toBeUndefined(); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + storageService.removeStorageKey(undefined, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage type is null`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + storageService.removeStorageKey(null, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage has not the given key`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + spyOn(storageService, 'storageHasKey').and.returnValue(false); + storageService.removeStorageKey(sessionType, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage is not supported`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + spyOn(storageService, 'storageIsSupported').and.returnValue(undefined); + storageService.removeStorageKey(sessionType, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage is not available`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + expectedMockStorage.setItem(expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + + spyOn(storageService, 'storageIsAvailable').and.returnValue(undefined); + storageService.removeStorageKey(sessionType, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + }); + }); +}); diff --git a/src/app/core/services/storage-service/storage.service.ts b/src/app/core/services/storage-service/storage.service.ts new file mode 100644 index 0000000000..50c27e9636 --- /dev/null +++ b/src/app/core/services/storage-service/storage.service.ts @@ -0,0 +1,152 @@ +import { Injectable } from '@angular/core'; + +/** + * The StorageType enumeration. + * + * It stores the possible storage types. + */ +export enum StorageType { + localStorage = 'localStorage', + sessionStorage = 'sessionStorage' +} + +/** + * The Storage service. + * + * It handles the storage of data + * in the session- or localstorage. + * + * Provided in: `root`. + */ +@Injectable({ + providedIn: 'root' +}) +export class StorageService { + /** + * Constructor of the StorageService. + */ + constructor() {} + + /** + * Public method: setStorageKey. + * + * It sets a given key/item string pair to a given storage type. + * + * @param {string} type The given storage type. + * @param {string} key The given key. + * @param {string} item The given item. + * + * @returns {void} It sets the storage key. + */ + setStorageKey(type: StorageType, key: string, item: string): void { + if (!type || !key || !item) { + return; + } + const storage = window[type]; + if (this.storageIsSupported(storage)) { + storage.setItem(key, item); + } else { + return; + } + } + + /** + * Public method: getStorageKey. + * + * It gets one item by key from a given storage type. + * + * @param {string} type The given storage type. + * @param {string} key The given key. + * + * @returns {string} The item from the storage. + */ + getStorageKey(type: StorageType, key: string): string { + if (!type || !key) { + return null; + } + const storage = window[type]; + if (this.storageHasKey(storage, key)) { + return storage.getItem(key); + } else { + return null; + } + } + + /** + * Public method: removeStorageKey. + * + * It removes a key from a given storage type. + * + * @param {string} type The given storage type. + * @param {string} key The given key. + * + * @returns {void} Removes a key pair from the storage. + */ + removeStorageKey(type: StorageType, key: string): void { + if (!type || !key) { + return; + } + const storage = window[type]; + if (this.storageHasKey(storage, key)) { + storage.removeItem(key); + } else { + return; + } + } + + /** + * Private method: storageHasKey. + * + * It checks if a given storage type has a given key. + * + * @param {any} storage The given storage type. + * @param {string} key The given key. + * + * @returns {boolean} The boolean value for the given key in the given storage type. + */ + private storageHasKey(storage: Storage, key: string): boolean { + if (this.storageIsSupported(storage)) { + return !!storage.getItem(key); + } + return false; + } + + /** + * Private method: storageIsSupported. + * + * It checks if a given storage type is supported. + * + * @param {Storage} storage The given storage type. + * + * @returns {Storage} The local reference to Storage for the given storage type. + */ + private storageIsSupported(storage: Storage): Storage { + return typeof storage !== 'undefined' && storage !== null && this.storageIsAvailable(storage); + } + + /** + * Private method: storageIsAvailable. + * + * It checks if a given storage type is available. + * cf. https://mathiasbynens.be/notes/localstorage-pattern + * + * @param {Storage} storage The given storage type. + * + * @returns {Storage} The local reference to Storage for the given storage type. + */ + private storageIsAvailable(storage: Storage): Storage { + try { + // make uid from Date + const uid = new Date().toDateString(); + + // set, get and remove item + storage.setItem(uid, uid); + const result = storage.getItem(uid) === uid; + + storage.removeItem(uid); + + // return local reference to Storage or undefined + return result && storage; + } catch (e) {} + } +} diff --git a/src/app/shared/modal/modal.component.ts b/src/app/shared/modal/modal.component.ts index b78d1d2737..3c9cbc79ec 100644 --- a/src/app/shared/modal/modal.component.ts +++ b/src/app/shared/modal/modal.component.ts @@ -15,6 +15,8 @@ const MODALCONTENTSNIPPETS = { 'Die edierten Notentexte von Aa:SkI/1, Ab:SkII/1, Ac:SkIII/1 und Ac:SkIII/7 sowie Ae:SkIV/1 erscheinen im Zusammenhang der vollständigen Edition der Vier Lieder op. 12 in AWG I/5.', op12_editionComingSoon: '

Die Einleitungen, edierten Notentexte und Kritischen Berichte zu

  • Werkedition der Druckfassung der Vier Lieder op. 12
    Textedition von Nr. I „Der Tag ist vergangen“ (Fassung 1)
    Textedition von Nr. I „Der Tag ist vergangen“ (Fassung 2)
    Textedition von Nr. IV Gleich und Gleich (Fassung 1)

erscheinen im Zusammenhang der vollständigen Edition der Vier Lieder op. 12 in AWG I/5.

', + op25_sheetComingSoon: + 'Die edierten Notentexte weiterer Skizzen der Drei Lieder nach Gedichten von Hildegard Jone op. 25 erscheinen in Kürze (02/2020).', op25_sourceNotA: '

Die Beschreibung der Quellen B sowie D–E einschließlich der darin gegebenenfalls enthaltenen Korrekturen erfolgt im Zusammenhang der vollständigen Edition der Drei Lieder nach Gedichten von Hildegard Jone op. 25 in AWG I/5.

', M198: diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html index 2e7d081ced..67e78a17bb 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html @@ -8,7 +8,7 @@

Objektdaten

  • - {{ prop?.label }} + {{ setLabel(prop) }}
      diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts index fa7dd70023..280ce463fb 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts @@ -14,7 +14,7 @@ import { ResourceDetailProperty } from '@awg-views/data-view/models'; import { ResourceDetailHtmlContentPropsComponent } from './props.component'; -describe('ResourceDetailHtmlContentPropsComponent (DONE)', () => { +describe('ResourceDetailHtmlContentPropsComponent', () => { let component: ResourceDetailHtmlContentPropsComponent; let fixture: ComponentFixture; let compDe: DebugElement; diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts index 2f1441e016..e7bfda9e62 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts @@ -1,6 +1,8 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { GndEvent, GndEventType } from '@awg-core/services/gnd-service'; import { ResourceDetailProperty } from '@awg-views/data-view/models'; +import { PropertyJson } from '@awg-shared/api-objects'; /** * The ResourceDetailHtmlContentProps component. @@ -14,7 +16,7 @@ import { ResourceDetailProperty } from '@awg-views/data-view/models'; styleUrls: ['./props.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ResourceDetailHtmlContentPropsComponent implements OnInit { +export class ResourceDetailHtmlContentPropsComponent implements OnInit, OnDestroy { /** * Input variable: props. * @@ -23,6 +25,14 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit { @Input() props: ResourceDetailProperty[]; + /** + * Output variable: gndRequest. + * + * It keeps an event emitter for the exposition of a GND value. + */ + @Output() + gndRequest: EventEmitter = new EventEmitter(); + /** * Output variable: resourceRequest. * @@ -64,4 +74,57 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit { id = id.toString(); this.resourceRequest.emit(id); } + + /** + * Public method: exposeGnd. + * + * It emits a given gnd event (type, value) + * to the {@link gndRequest}. + * + * @param {GndEvent} gndEvent The given GND event. + * + * @returns {void} Emits the GND event. + */ + exposeGnd(gndEvent: GndEvent): void { + if (!gndEvent) { + return; + } + this.gndRequest.emit(gndEvent); + } + + /** + * Public method: setLabel. + * + * It sets the label of a given property object + * while checking for a gnd value to expose. + * + * @param {PropertyJson} prop The given property object. + * + * @returns {string} The property label. + */ + setLabel(prop: PropertyJson): string { + if (!prop) { + return null; + } + // if we have a gnd (prop.pid=856), write it to sessionStorage + if (prop.pid === '856' && prop.values && prop.values.length > 0) { + prop.values.map((value: string) => { + const gndEvent = new GndEvent(GndEventType.set, value); + this.exposeGnd(gndEvent); + }); + } + return prop.label; + } + + /** + * Angular life cycle hook: ngOnDestroy. + * + * It calls the containing methods + * when destroying the component. + */ + ngOnDestroy() { + // if we leave the component, remove gnd from storage + const gndEvent = new GndEvent(GndEventType.remove, null); + this.exposeGnd(gndEvent); + } } diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html index d3fd0a9222..88353f4981 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html @@ -19,6 +19,7 @@
      diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts index 4b9583c253..cc9cba6d8b 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts @@ -12,6 +12,7 @@ import { } from '@testing/expect-helper'; import { mockContextJson } from '@testing/mock-data'; +import { GndEvent } from '@awg-core/services/gnd-service'; import { ContextJson } from '@awg-shared/api-objects'; import { ResourceDetailContent, @@ -28,6 +29,8 @@ class ResourceDetailHtmlContentPropsStubComponent { @Input() props: ResourceDetailProperty[]; @Output() + gndRequest: EventEmitter = new EventEmitter(); + @Output() resourceRequest: EventEmitter = new EventEmitter(); } diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts index aef9274aa7..e4b5dc272e 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ResourceDetailContent } from '@awg-views/data-view/models'; +import { GndEvent } from '@awg-core/services/gnd-service'; /** * The ResourceDetailHtmlContent component. @@ -26,6 +27,14 @@ export class ResourceDetailHtmlContentComponent implements OnInit { @Input() content: ResourceDetailContent; + /** + * Output variable: gndRequest. + * + * It keeps an event emitter for the exposition of a GND value. + */ + @Output() + gndRequest: EventEmitter = new EventEmitter(); + /** * Output variable: resourceRequest. * @@ -42,6 +51,23 @@ export class ResourceDetailHtmlContentComponent implements OnInit { */ ngOnInit() {} + /** + * Public method: exposeGnd. + * + * It emits a given gnd event (type, value) + * to the {@link gndRequest}. + * + * @param {{type: string, value: string}} gndEvent The given event. + * + * @returns {void} Emits the event. + */ + exposeGnd(gndEvent: GndEvent): void { + if (!gndEvent) { + return; + } + this.gndRequest.emit(gndEvent); + } + /** * Public method: navigateToResource. * diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html index e5956ea95f..2e2215dd4f 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html @@ -2,6 +2,7 @@ diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts index eba549fd2c..a2a022d207 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts @@ -13,6 +13,7 @@ import { ResourceDetailImage, ResourceDetailProperty } from '@awg-views/data-view/models'; +import { GndEvent } from '@awg-core/services/gnd-service'; import { ResourceDetailHtmlComponent } from './resource-detail-html.component'; @@ -22,6 +23,8 @@ class ResourceDetailHtmlContentStubComponent { @Input() content: ResourceDetailContent; @Output() + gndRequest: EventEmitter = new EventEmitter(); + @Output() resourceRequest: EventEmitter = new EventEmitter(); } diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts index fc9ea6dbaf..0b4aff42e0 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ResourceDetail } from '@awg-views/data-view/models'; +import { GndEvent } from '@awg-core/services/gnd-service'; /** * The ResourceDetailHtml component. @@ -24,6 +25,14 @@ export class ResourceDetailHtmlComponent implements OnInit { @Input() resourceDetailData: ResourceDetail; + /** + * Output variable: gndRequest. + * + * It keeps an event emitter for the exposition of a GND value. + */ + @Output() + gndRequest: EventEmitter = new EventEmitter(); + /** * Output variable: resourceRequest. * @@ -40,6 +49,23 @@ export class ResourceDetailHtmlComponent implements OnInit { */ ngOnInit() {} + /** + * Public method: exposeGnd. + * + * It emits a given gnd event (type, value) + * to the {@link gndRequest}. + * + * @param {{type: string, value: string}} gndEvent The given event. + * + * @returns {void} Emits the event. + */ + exposeGnd(gndEvent: GndEvent): void { + if (!gndEvent) { + return; + } + this.gndRequest.emit(gndEvent); + } + /** * Public method: navigateToResource. * diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html index 2af6044f38..6795a59c6c 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html @@ -33,6 +33,7 @@ diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts index 081ec14865..34734a6ad2 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts @@ -11,6 +11,7 @@ import { NgbTabsetModule } from '@ng-bootstrap/ng-bootstrap'; import Spy = jasmine.Spy; import { DataStreamerService, LoadingService } from '@awg-core/services'; +import { GndEvent } from '@awg-core/services/gnd-service'; import { DataApiService } from '@awg-views/data-view/services'; import { ResourceFullResponseJson } from '@awg-shared/api-objects'; @@ -34,6 +35,8 @@ class ResourceDetailHtmlStubComponent { @Input() resourceDetailData: ResourceDetail; @Output() + gndRequest: EventEmitter = new EventEmitter(); + @Output() resourceRequest: EventEmitter = new EventEmitter(); } diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts index 97af269869..1161799bed 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts @@ -6,7 +6,8 @@ import { switchMap, takeUntil } from 'rxjs/operators'; import { NgbTabsetConfig } from '@ng-bootstrap/ng-bootstrap'; -import { DataStreamerService, LoadingService } from '@awg-core/services'; +import { DataStreamerService, GndService, LoadingService } from '@awg-core/services'; +import { GndEvent, GndEventType } from '@awg-core/services/gnd-service'; import { DataApiService } from '@awg-views/data-view/services'; import { ResourceData } from '@awg-views/data-view/models'; @@ -96,13 +97,14 @@ export class ResourceDetailComponent implements OnInit, OnDestroy { * * It declares private instances of the Angular ActivatedRoute, * the Angular Router, the DataApiService, the DataStreamerService, - * the LoadingService, and a configuration object for the + * the GndService, the LoadingService, and a configuration object for the * ng-bootstrap tabset. * * @param {ActivatedRoute} route Instance of the Angular ActivatedRoute. * @param {Router} router Instance of the Angular Router. * @param {DataApiService} dataApiService Instance of the DataApiService. * @param {DataStreamerService} dataStreamerService Instance of the DataStreamerService. + * @param {GndService} gndService Instance of the GndService. * @param {LoadingService} loadingService Instance of the LoadingService. * @param {NgbTabsetConfig} config Instance of the NgbTabsetConfig. */ @@ -111,6 +113,7 @@ export class ResourceDetailComponent implements OnInit, OnDestroy { private router: Router, private dataApiService: DataApiService, private dataStreamerService: DataStreamerService, + private gndService: GndService, private loadingService: LoadingService, config: NgbTabsetConfig ) { @@ -203,6 +206,38 @@ export class ResourceDetailComponent implements OnInit, OnDestroy { this.router.navigate(['/data/resource', +nextId]); } + /** + * Public method: exposeGnd. + * + * It delegates a given gnd event type ('set', 'get', 'remove') + * with a given value to the {@link GndService}. + * The gnd event is emitted from the {@link ResourceDetailHtmlContentPropsComponent}. + * + * @param {{type: string, value: string}} gndEvent The given event. + * + * @returns {void} Delegates the event to the GndService. + */ + exposeGnd(gndEvent: GndEvent): void { + if (!gndEvent) { + return; + } + switch (gndEvent.type) { + case GndEventType.set: { + // statements + this.gndService.setGndToSessionStorage(gndEvent.value); + break; + } + case GndEventType.remove: { + // statements + this.gndService.removeGndFromSessionStorage(); + break; + } + default: { + console.log('got an uncatched GND event', gndEvent); + } + } + } + /** * Public method: routeToSidenav. * diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-accolade/edition-accolade.component.html b/src/app/views/edition-view/edition-outlets/edition-detail/edition-accolade/edition-accolade.component.html index 9cd3807368..a2e3f4b6ec 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-accolade/edition-accolade.component.html +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-accolade/edition-accolade.component.html @@ -15,7 +15,8 @@