From 84648c990efdeea66891a146ab109219290dca88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2020 13:51:54 +0000 Subject: [PATCH 01/30] build(deps): bump handlebars from 4.1.2 to 4.7.3 Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.7.3. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.7.3) Signed-off-by: dependabot[bot] --- yarn.lock | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index 70ad680dde..a2c21dbef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3600,7 +3600,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.12.1, commander@^2.18.0, commander@^2.20.0, commander@~2.20.0: +commander@^2.11.0, commander@^2.12.1, commander@^2.18.0, commander@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== @@ -3610,6 +3610,11 @@ commander@^3.0.0-0, commander@^3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -5983,21 +5988,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== - dependencies: - neo-async "^2.6.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - -handlebars@^4.3.3, handlebars@^4.4.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== +handlebars@^4.1.2, handlebars@^4.3.3, handlebars@^4.4.0: + version "4.7.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" + integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -11307,11 +11301,11 @@ typescript@^2.4.2: integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + version "3.7.7" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.7.tgz#21e52c7dccda80a53bf7cde69628a7e511aec9c9" + integrity sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA== dependencies: - commander "~2.20.0" + commander "~2.20.3" source-map "~0.6.1" ultron@~1.1.0: From 9f1c9fc07960335d2e4c5c8bec0f7ace44054a9f Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Thu, 6 Feb 2020 15:27:24 +0100 Subject: [PATCH 02/30] fix(home): adjust title of op. 25 --- src/app/views/home-view/home-view.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/views/home-view/home-view.component.html b/src/app/views/home-view/home-view.component.html index 1c531520ef..978b6ab6a4 100644 --- a/src/app/views/home-view/home-view.component.html +++ b/src/app/views/home-view/home-view.component.html @@ -8,7 +8,7 @@
AWG / Serie I (Werke mit Opuszahlen) / Abteilung 5 (Klavierlieder)

Vier Lieder op. 12 (Skizzen)

-

Drei Lieder op. 25 (Graph)

+

Drei Lieder nach Gedichten von Hildegard Jone op. 25 (Graph)

From bde1f1eff3b49341d1ebf621cc5f535b4e9ec1e4 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Thu, 6 Feb 2020 20:55:51 +0100 Subject: [PATCH 03/30] feat(core): add StorageService Fixes #5 --- .../core-service/core.service.spec.ts | 2 +- src/app/core/services/index.ts | 11 +- .../core/services/storage-service/index.ts | 1 + .../storage-service/storage.service.spec.ts | 286 ++++++++++++++++++ .../storage-service/storage.service.ts | 140 +++++++++ 5 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 src/app/core/services/storage-service/index.ts create mode 100644 src/app/core/services/storage-service/storage.service.spec.ts create mode 100644 src/app/core/services/storage-service/storage.service.ts 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/index.ts b/src/app/core/services/index.ts index c9b751bdc6..8246c7b420 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -13,5 +13,14 @@ import { CoreService } from './core-service'; import { DataStreamerService } from './data-streamer'; import { LoadingService } from './loading-service'; import { SideInfoService } from './side-info-service'; +import { StorageService } from './storage-service'; -export { ApiService, ConversionService, CoreService, DataStreamerService, LoadingService, SideInfoService }; +export { + ApiService, + ConversionService, + CoreService, + DataStreamerService, + LoadingService, + SideInfoService, + StorageService +}; 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..488705d66e --- /dev/null +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -0,0 +1,286 @@ +import { TestBed } from '@angular/core/testing'; + +import Spy = jasmine.Spy; + +import { expectSpyCall } from '@testing/expect-helper'; + +import { StorageService, StorageType } from './storage.service'; +import { throwError } from 'rxjs'; + +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]; + + let setItemSpy: Spy; + + 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); + setItemSpy = 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 mockSesscionStorage', () => { + 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`, () => { + 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; + + 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 return null for non existing prev item`, () => { + const expectedPrevKey = expectedKey + '_prev'; + const getStorageKeySpy = spyOn(storageService, 'getStorageKey').and.callThrough(); + + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expectSpyCall(getStorageKeySpy, 1, sessionType); + expect(expectedMockStorage.getItem(expectedPrevKey)).toBe('null'); + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`... should set correct prev item when existing`, () => { + const expectedPrevKey = expectedKey + '_prev'; + const getStorageKeySpy = spyOn(storageService, 'getStorageKey').and.callThrough(); + + expectedMockStorage.setItem(expectedKey, otherItem); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expectSpyCall(getStorageKeySpy, 1, sessionType); + expect(expectedMockStorage.getItem(expectedPrevKey)).toEqual(otherItem, `should be ${otherItem}`); + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + storageService.setStorageKey(undefined, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage type is null`, () => { + storageService.setStorageKey(null, expectedKey, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage is not available`, () => { + spyOn(storageService, 'storageIsAvailable').and.returnValue(false); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- storage is not supported`, () => { + spyOn(storageService, 'storageIsSupported').and.returnValue(false); + storageService.setStorageKey(sessionType, expectedKey, expectedItem); + + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- key is undefined `, () => { + storageService.setStorageKey(sessionType, undefined, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- key is null`, () => { + storageService.setStorageKey(sessionType, null, expectedItem); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- value is undefined `, () => { + storageService.setStorageKey(sessionType, expectedKey, undefined); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + + it(`- value is null`, () => { + storageService.setStorageKey(sessionType, expectedKey, null); + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); + }); + }); + }); + + describe('#getStorageKey', () => { + it(`... should get an item by key from a given storage type`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + expect(storageService.getStorageKey(sessionType, expectedKey)).toEqual( + expectedItem, + `should be ${expectedItem}` + ); + }); + + it('... should return null for non existing items', () => { + expect(expectedMockStorage.getItem('key')).toBeNull(); // null + expect(storageService.getStorageKey(sessionType, 'key')).toBeNull(); + }); + + it(`... should get item from the correct storage type`, () => { + const otherStorage = expectedLocalStorage; + + 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}`); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + expect(storageService.getStorageKey(undefined, expectedKey)).toBeNull(); + }); + + it(`- storage type is null`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + expect(storageService.getStorageKey(null, expectedKey)).toBeNull(); + }); + + it(`- storage has not the given key`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + + spyOn(storageService, 'storageHasKey').and.returnValue(false); + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`- storage is not supported`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + spyOn(storageService, 'storageIsSupported').and.returnValue(false); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`- storage is not available`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + spyOn(storageService, 'storageIsAvailable').and.returnValue(false); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`- storage is not available (DOMException)`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + + const expectedError = new DOMException('oh noes', 'QuotaExceededError'); + console.log(expectedError.name, expectedError.code, expectedError instanceof DOMException); + + spyOn(storageService, 'storageIsAvailable').and.callFake((storage: Storage) => { + console.log(storage); + try { + throw expectedError; + } catch (e) { + return ( + e instanceof DOMException && + (e.code === 22 || + e.code === 1014 || + e.name === 'QuotaExceededError' || + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + storage && + storage.length !== 0 + ); + } + }); + + expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); + }); + }); + }); +}); 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..5754804ef8 --- /dev/null +++ b/src/app/core/services/storage-service/storage.service.ts @@ -0,0 +1,140 @@ +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)) { + const prevItem = this.getStorageKey(type, key); + storage.setItem(key + '_prev', prevItem); + 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; + } + } + + /** + * 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 {boolean} The boolean value for the given storage type. + */ + private storageIsSupported(storage: Storage): boolean { + return typeof storage !== 'undefined' && storage !== null && this.storageIsAvailable(storage); + } + + /** + * Private method: storageIsAvailable. + * + * It checks if a given storage type is available. + * + * @param {Storage} storage The given storage type. + * + * @returns {boolean} The boolean value for the given storage type. + */ + private storageIsAvailable(storage: Storage): boolean { + try { + const x = '__storage_test__'; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } catch (e) { + return ( + e instanceof DOMException && + // everything except Firefox + (e.code === 22 || + // Firefox + e.code === 1014 || + // test name field too, because code might not be present + // everything except Firefox + e.name === 'QuotaExceededError' || + // Firefox + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + // acknowledge QuotaExceededError only if there's something already stored + storage && + storage.length !== 0 + ); + } + } +} From b5c4c08bf370e9bb6048fa9775ab30ea8690ce45 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Thu, 6 Feb 2020 22:18:07 +0100 Subject: [PATCH 04/30] fix(core): fix check for detecting Storage --- .../storage-service/storage.service.spec.ts | 30 +------------- .../storage-service/storage.service.ts | 41 ++++++++----------- 2 files changed, 18 insertions(+), 53 deletions(-) diff --git a/src/app/core/services/storage-service/storage.service.spec.ts b/src/app/core/services/storage-service/storage.service.spec.ts index 488705d66e..ec44315ee2 100644 --- a/src/app/core/services/storage-service/storage.service.spec.ts +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -244,40 +244,14 @@ describe('StorageService', () => { it(`- storage is not supported`, () => { expectedMockStorage.setItem(expectedKey, expectedItem); - spyOn(storageService, 'storageIsSupported').and.returnValue(false); + spyOn(storageService, 'storageIsSupported').and.returnValue(undefined); expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); }); it(`- storage is not available`, () => { expectedMockStorage.setItem(expectedKey, expectedItem); - spyOn(storageService, 'storageIsAvailable').and.returnValue(false); - - expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); - }); - - it(`- storage is not available (DOMException)`, () => { - expectedMockStorage.setItem(expectedKey, expectedItem); - - const expectedError = new DOMException('oh noes', 'QuotaExceededError'); - console.log(expectedError.name, expectedError.code, expectedError instanceof DOMException); - - spyOn(storageService, 'storageIsAvailable').and.callFake((storage: Storage) => { - console.log(storage); - try { - throw expectedError; - } catch (e) { - return ( - e instanceof DOMException && - (e.code === 22 || - e.code === 1014 || - e.name === 'QuotaExceededError' || - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - storage && - storage.length !== 0 - ); - } - }); + spyOn(storageService, 'storageIsAvailable').and.returnValue(undefined); expect(storageService.getStorageKey(sessionType, expectedKey)).toBeNull(); }); diff --git a/src/app/core/services/storage-service/storage.service.ts b/src/app/core/services/storage-service/storage.service.ts index 5754804ef8..c67ab570cc 100644 --- a/src/app/core/services/storage-service/storage.service.ts +++ b/src/app/core/services/storage-service/storage.service.ts @@ -98,9 +98,9 @@ export class StorageService { * * @param {Storage} storage The given storage type. * - * @returns {boolean} The boolean value for the given storage type. + * @returns {Storage} The local reference to Storage for the given storage type. */ - private storageIsSupported(storage: Storage): boolean { + private storageIsSupported(storage: Storage): Storage { return typeof storage !== 'undefined' && storage !== null && this.storageIsAvailable(storage); } @@ -108,33 +108,24 @@ export class StorageService { * 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 {boolean} The boolean value for the given storage type. + * @returns {boolean} The local reference to Storage for the given storage type. */ - private storageIsAvailable(storage: Storage): boolean { + private storageIsAvailable(storage: Storage): Storage { try { - const x = '__storage_test__'; - storage.setItem(x, x); - storage.removeItem(x); - return true; - } catch (e) { - return ( - e instanceof DOMException && - // everything except Firefox - (e.code === 22 || - // Firefox - e.code === 1014 || - // test name field too, because code might not be present - // everything except Firefox - e.name === 'QuotaExceededError' || - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - // acknowledge QuotaExceededError only if there's something already stored - storage && - storage.length !== 0 - ); - } + // make uid from Date + const uid = JSON.stringify(new Date()); + + // 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) {} } } From c534509acc03b799c8363dbb300bb6cbdd8f0794 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 00:09:43 +0100 Subject: [PATCH 05/30] build(app): add posttest:cov script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 321a2a755f..a6f2deb5ca 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "start": "ng serve", "e2e": "ng e2e", "test": "ng test", - "test:cov": "ng test awg-app --code-coverage", + "test:cov": "ng test --code-coverage true", + "posttest: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", From 23e365690b35b6ec9f7169401f1d9fbe35883db9 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 01:44:04 +0100 Subject: [PATCH 06/30] feat(core): add RouterEventsService to store previous route --- src/app/app.component.spec.ts | 17 ++++- src/app/app.component.ts | 19 ++++- src/app/app.config.ts | 9 +++ src/app/core/services/index.ts | 2 + .../services/router-events-service/index.ts | 1 + .../router-events.service.spec.ts | 22 ++++++ .../router-events.service.ts | 69 +++++++++++++++++++ 7 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/app/core/services/router-events-service/index.ts create mode 100644 src/app/core/services/router-events-service/router-events.service.spec.ts create mode 100644 src/app/core/services/router-events-service/router-events.service.ts 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..7e10d678b3 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,25 @@ 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) { + /* + // set current route to sessionStorage + this.storageService.setStorageKey( + StorageType.sessionStorage, + AppConfig.AWG_APP_ROUTE_STORAGE_KEY, + event.urlAfterRedirects + ); + */ + + // 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/core/services/index.ts b/src/app/core/services/index.ts index 8246c7b420..4cd91cc4f6 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -12,6 +12,7 @@ import { ConversionService } from './conversion-service'; import { CoreService } from './core-service'; import { DataStreamerService } from './data-streamer'; import { LoadingService } from './loading-service'; +import { RouterEventsService } from './router-events-service'; import { SideInfoService } from './side-info-service'; import { StorageService } from './storage-service'; @@ -21,6 +22,7 @@ export { CoreService, DataStreamerService, 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..2220d18a10 --- /dev/null +++ b/src/app/core/services/router-events-service/router-events.service.ts @@ -0,0 +1,69 @@ +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 StorageService. + * + * 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); + console.log('event', this.previousRoutePathSubject.value); + }); + } + + /** + * 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$; + } +} From 19a6f8d85548782397e836cd8fd802fb1d840628 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 02:07:22 +0100 Subject: [PATCH 07/30] fix(core): add removeItem method to StorageService --- .../storage-service/storage.service.spec.ts | 82 +++++++++++++++++-- .../storage-service/storage.service.ts | 22 +++++ 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/app/core/services/storage-service/storage.service.spec.ts b/src/app/core/services/storage-service/storage.service.spec.ts index ec44315ee2..5c03ce54ba 100644 --- a/src/app/core/services/storage-service/storage.service.spec.ts +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -17,8 +17,6 @@ describe('StorageService', () => { const expectedLocalStorage: Storage = window[localType]; const expectedSessionStorage: Storage = window[sessionType]; - let setItemSpy: Spy; - const expectedKey = 'key'; const expectedItem = 'expectedItem'; const otherItem = 'otherItem'; @@ -52,7 +50,7 @@ describe('StorageService', () => { // spies replace storage calls with fake mockStorage calls spyOn(localStorage, 'getItem').and.callFake(mockStorage.getItem); - setItemSpy = spyOn(localStorage, 'setItem').and.callFake(mockStorage.setItem); + spyOn(localStorage, 'setItem').and.callFake(mockStorage.setItem); spyOn(localStorage, 'removeItem').and.callFake(mockStorage.removeItem); spyOn(localStorage, 'clear').and.callFake(mockStorage.clear); }); @@ -86,7 +84,7 @@ describe('StorageService', () => { expect(expectedMockStorage.getItem('foo')).toBeNull(); // null }); - it('... should set, get items and clear mockSesscionStorage', () => { + it('... should set, get items and clear mockSessionStorage', () => { expectedMockStorage = expectedSessionStorage; expect(expectedMockStorage.setItem('foo', 'bar')); @@ -207,7 +205,7 @@ describe('StorageService', () => { }); it('... should return null for non existing items', () => { - expect(expectedMockStorage.getItem('key')).toBeNull(); // null + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); // null expect(storageService.getStorageKey(sessionType, 'key')).toBeNull(); }); @@ -257,4 +255,78 @@ describe('StorageService', () => { }); }); }); + + describe('#removeStorageKey', () => { + it(`... should remove an item by key from a given storage type`, () => { + 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 return null for non existing items', () => { + expect(storageService.removeStorageKey(sessionType, expectedKey)).toBeNull(); + }); + + it(`... should remove item from the correct storage type`, () => { + const otherStorage = expectedLocalStorage; + 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(); + }); + + describe(`... should do nothing if:`, () => { + it(`- storage type is undefined `, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + + storageService.removeStorageKey(undefined, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage type is null`, () => { + expectedMockStorage.setItem(expectedKey, expectedItem); + + storageService.removeStorageKey(null, expectedKey); + + expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); + }); + + it(`- storage has not the given key`, () => { + expectedMockStorage.setItem(expectedKey, 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`, () => { + expectedMockStorage.setItem(expectedKey, 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`, () => { + expectedMockStorage.setItem(expectedKey, 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 index c67ab570cc..fd64b48e17 100644 --- a/src/app/core/services/storage-service/storage.service.ts +++ b/src/app/core/services/storage-service/storage.service.ts @@ -74,6 +74,28 @@ export class StorageService { } } + /** + * 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 null; + } + const storage = window[type]; + if (this.storageHasKey(storage, key)) { + return storage.removeItem(key); + } else { + return null; + } + } + /** * Private method: storageHasKey. * From e591885d64977ae9ef87ab6bdc58193f323adefd Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 08:23:34 +0100 Subject: [PATCH 08/30] fix(core): use StorageService to expose GND --- .../core/services/api-service/api.service.ts | 2 +- .../conversion-service/conversion.service.ts | 34 +++++++++++++---- .../storage-service/storage.service.spec.ts | 38 ++++++++----------- .../storage-service/storage.service.ts | 8 ++-- 4 files changed, 46 insertions(+), 36 deletions(-) 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..1a68a6065b 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -1,10 +1,13 @@ 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 { StorageService, StorageType } from '@awg-core/services/storage-service'; + import { GeoNames } from '@awg-core/core-models'; import { ContextJson, @@ -69,6 +72,20 @@ 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. + * @param {StorageService} storageService Instance of the StorageService. + */ + constructor(public http: HttpClient, private storageService: StorageService) { + super(http); + } + /** * Public method: convertFullTextSearchResults. * @@ -496,7 +513,7 @@ export class ConversionService extends ApiService { 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]); + this.writeGndToSessionStorage(prop.values[i]); } // convert richtext standoff prop.toHtml[i] = this.convertRichtextValue(prop.values[i].utf8str, prop.values[i].textattr); @@ -952,16 +969,19 @@ export class ConversionService extends ApiService { } /** - * Private method: writeGndToLocalStorage. + * Private method: writeGndToSessionStorage. * - * It writes the GND number to the localStorage + * It writes the GND number to the sessionStorage * for further processing. * * @param {} value The given incoming property value. * - * @returns {void} Writes the GND number to the localStorage. + * @returns {void} Writes the GND number to the sessionStorage. */ - private writeGndToLocalStorage(value: PropertyJsonValue): void { + private writeGndToSessionStorage(value: PropertyJsonValue): void { + if (!value) { + return; + } const dnbReg = 'http://d-nb.info/gnd/'; const gndKey = 'gnd'; let gndItem: string; @@ -973,9 +993,9 @@ export class ConversionService extends ApiService { 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); + this.storageService.setStorageKey(StorageType.sessionStorage, gndKey, gndItem); } else { - localStorage.removeItem(gndKey); + this.storageService.removeStorageKey(StorageType.sessionStorage, gndKey); } } } diff --git a/src/app/core/services/storage-service/storage.service.spec.ts b/src/app/core/services/storage-service/storage.service.spec.ts index 5c03ce54ba..188359449c 100644 --- a/src/app/core/services/storage-service/storage.service.spec.ts +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -125,27 +125,19 @@ describe('StorageService', () => { expect(otherStorage.getItem(expectedKey)).toEqual(otherItem, `should be ${otherItem}`); }); - it(`... should return null for non existing prev item`, () => { - const expectedPrevKey = expectedKey + '_prev'; - const getStorageKeySpy = spyOn(storageService, 'getStorageKey').and.callThrough(); - + it(`... should set a new key/item when a key does not exist`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); storageService.setStorageKey(sessionType, expectedKey, expectedItem); - expectSpyCall(getStorageKeySpy, 1, sessionType); - expect(expectedMockStorage.getItem(expectedPrevKey)).toBe('null'); expect(expectedMockStorage.getItem(expectedKey)).toEqual(expectedItem, `should be ${expectedItem}`); }); - it(`... should set correct prev item when existing`, () => { - const expectedPrevKey = expectedKey + '_prev'; - const getStorageKeySpy = spyOn(storageService, 'getStorageKey').and.callThrough(); - - expectedMockStorage.setItem(expectedKey, otherItem); + it(`... should overwrite an existing item with the correct item when a key exists`, () => { storageService.setStorageKey(sessionType, expectedKey, expectedItem); - - expectSpyCall(getStorageKeySpy, 1, sessionType); - expect(expectedMockStorage.getItem(expectedPrevKey)).toEqual(otherItem, `should be ${otherItem}`); 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:`, () => { @@ -204,11 +196,6 @@ describe('StorageService', () => { ); }); - it('... should return null for non existing items', () => { - expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); // null - expect(storageService.getStorageKey(sessionType, 'key')).toBeNull(); - }); - it(`... should get item from the correct storage type`, () => { const otherStorage = expectedLocalStorage; @@ -222,6 +209,11 @@ describe('StorageService', () => { expect(storageService.getStorageKey(localType, expectedKey)).toEqual(otherItem, `should be ${otherItem}`); }); + it('... should return null for non existing items', () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); // null + expect(storageService.getStorageKey(sessionType, 'key')).toBeNull(); + }); + describe(`... should do nothing if:`, () => { it(`- storage type is undefined `, () => { expectedMockStorage.setItem(expectedKey, expectedItem); @@ -265,10 +257,6 @@ describe('StorageService', () => { expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); }); - it('... should return null for non existing items', () => { - expect(storageService.removeStorageKey(sessionType, expectedKey)).toBeNull(); - }); - it(`... should remove item from the correct storage type`, () => { const otherStorage = expectedLocalStorage; expectedMockStorage.setItem(expectedKey, expectedItem); @@ -284,6 +272,10 @@ describe('StorageService', () => { expect(otherStorage.getItem(expectedKey)).toBeNull(); }); + it('... should return for non existing items', () => { + expect(storageService.removeStorageKey(sessionType, expectedKey)).toBeUndefined(); + }); + describe(`... should do nothing if:`, () => { it(`- storage type is undefined `, () => { expectedMockStorage.setItem(expectedKey, expectedItem); diff --git a/src/app/core/services/storage-service/storage.service.ts b/src/app/core/services/storage-service/storage.service.ts index fd64b48e17..4551aa904f 100644 --- a/src/app/core/services/storage-service/storage.service.ts +++ b/src/app/core/services/storage-service/storage.service.ts @@ -44,8 +44,6 @@ export class StorageService { } const storage = window[type]; if (this.storageIsSupported(storage)) { - const prevItem = this.getStorageKey(type, key); - storage.setItem(key + '_prev', prevItem); storage.setItem(key, item); } else { return; @@ -86,13 +84,13 @@ export class StorageService { */ removeStorageKey(type: StorageType, key: string): void { if (!type || !key) { - return null; + return; } const storage = window[type]; if (this.storageHasKey(storage, key)) { - return storage.removeItem(key); + storage.removeItem(key); } else { - return null; + return; } } From 52f5b4de8111a79bb0c2a7b0cfe270567c9ff158 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 09:38:52 +0100 Subject: [PATCH 09/30] refactor(core): name data streamer service path consistently --- .../data-streamer.service.spec.ts | 0 .../data-streamer.service.ts | 0 .../services/{data-streamer => data-streamer-service}/index.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/app/core/services/{data-streamer => data-streamer-service}/data-streamer.service.spec.ts (100%) rename src/app/core/services/{data-streamer => data-streamer-service}/data-streamer.service.ts (100%) rename src/app/core/services/{data-streamer => data-streamer-service}/index.ts (100%) 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 From e44b3324d092d2b799242c9a7b0d0a3cfce371a6 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Fri, 7 Feb 2020 09:41:00 +0100 Subject: [PATCH 10/30] fix(app): move GND exposition to PropsComp and GndService --- .../conversion-service/conversion.service.ts | 35 ---------------- .../services/gnd-service/gnd.service.spec.ts | 12 ++++++ .../core/services/gnd-service/gnd.service.ts | 40 +++++++++++++++++++ src/app/core/services/gnd-service/index.ts | 1 + src/app/core/services/index.ts | 4 +- .../router-events.service.ts | 3 +- .../props/props.component.html | 2 +- .../props/props.component.ts | 23 ++++++++++- 8 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 src/app/core/services/gnd-service/gnd.service.spec.ts create mode 100644 src/app/core/services/gnd-service/gnd.service.ts create mode 100644 src/app/core/services/gnd-service/index.ts diff --git a/src/app/core/services/conversion-service/conversion.service.ts b/src/app/core/services/conversion-service/conversion.service.ts index 1a68a6065b..52472259f7 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -511,10 +511,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.writeGndToSessionStorage(prop.values[i]); - } // convert richtext standoff prop.toHtml[i] = this.convertRichtextValue(prop.values[i].utf8str, prop.values[i].textattr); } @@ -967,35 +963,4 @@ export class ConversionService extends ApiService { links: incomingLinks.filter(incomingLink => incomingLink.restype.label === label) })); } - - /** - * Private method: writeGndToSessionStorage. - * - * It writes the GND number to the sessionStorage - * for further processing. - * - * @param {} value The given incoming property value. - * - * @returns {void} Writes the GND number to the sessionStorage. - */ - private writeGndToSessionStorage(value: PropertyJsonValue): void { - if (!value) { - return; - } - 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(); - this.storageService.setStorageKey(StorageType.sessionStorage, gndKey, gndItem); - } else { - this.storageService.removeStorageKey(StorageType.sessionStorage, gndKey); - } - } } 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..f3610bac14 --- /dev/null +++ b/src/app/core/services/gnd-service/gnd.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { GndService } from './gnd.service'; + +describe('GndService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: GndService = TestBed.get(GndService); + expect(service).toBeTruthy(); + }); +}); 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..2f2de20a13 --- /dev/null +++ b/src/app/core/services/gnd-service/gnd.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; + +import { StorageType, StorageService } from '@awg-core/services/storage-service'; + +@Injectable({ + providedIn: 'root' +}) +export class GndService { + readonly gndKey = 'gnd'; + + constructor(private storageService: StorageService) {} + + writeGndToSessionStorage(value: string): void { + const dnbReg = /href="(https?:\/\/d-nb.info\/gnd\/(\w{5,10}))"/i; // regexp for links + let linkRegArr: RegExpExecArray; + + let gndItem: string; + + const valueHasGnd = (checkValue: string): boolean => { + // check for dnbLink in value + if (dnbReg.exec(checkValue)) { + linkRegArr = dnbReg.exec(checkValue); + return !!linkRegArr; + } + return false; + }; + + if (valueHasGnd(value)) { + // split utf8str with gnd value into array and take last argument (pop) + gndItem = linkRegArr.pop(); + this.storageService.setStorageKey(StorageType.sessionStorage, this.gndKey, gndItem); + } else { + this.removeGndFromSessionStorage(); + } + } + + removeGndFromSessionStorage(): void { + this.storageService.removeStorageKey(StorageType.sessionStorage, this.gndKey); + } +} 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 4cd91cc4f6..50067ab69f 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -10,7 +10,8 @@ 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'; @@ -21,6 +22,7 @@ export { ConversionService, CoreService, DataStreamerService, + GndService, LoadingService, RouterEventsService, SideInfoService, 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 index 2220d18a10..575270dda9 100644 --- a/src/app/core/services/router-events-service/router-events.service.ts +++ b/src/app/core/services/router-events-service/router-events.service.ts @@ -28,7 +28,7 @@ export class RouterEventsService { private readonly previousRoutePath$ = this.previousRoutePathSubject.asObservable(); /** - * Constructor of the StorageService. + * Constructor of the RouterEventsService. * * It declares private instances of the Angular router and the Location. * It filters the router events fo recognized routes @@ -52,7 +52,6 @@ export class RouterEventsService { ) .subscribe((event: any[]) => { this.previousRoutePathSubject.next(event[0].urlAfterRedirects); - console.log('event', this.previousRoutePathSubject.value); }); } 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.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts index 2f1441e016..1c7cb5155a 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,5 +1,6 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { GndService } from '@awg-core/services'; import { ResourceDetailProperty } from '@awg-views/data-view/models'; /** @@ -14,7 +15,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. * @@ -39,6 +40,8 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit { */ metaBreakLine = 'Versionsdatum'; + constructor(private gndService: GndService) {} + /** * Angular life cycle hook: ngOnInit. * @@ -64,4 +67,20 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit { id = id.toString(); this.resourceRequest.emit(id); } + + setLabel(prop) { + if (!prop) { + return; + } + // if we have a gnd (prop.pid=856), write it to sessionStorage + if (prop.pid === '856' && prop.values && prop.values[0]) { + prop.values.map(value => this.gndService.writeGndToSessionStorage(value)); + } + return prop.label; + } + + ngOnDestroy() { + // if we leave the component, remove gnd from storage + this.gndService.removeGndFromSessionStorage(); + } } From 98bd896d6151c09d806fef1c82734e899a55d43d Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Fri, 7 Feb 2020 14:55:44 +0100 Subject: [PATCH 11/30] fix(app): move GND exposition to PropsComp and GndService --- src/app/app.component.ts | 9 -- .../conversion-service/conversion.service.ts | 5 +- .../core/services/gnd-service/gnd.service.ts | 126 +++++++++++++++--- .../storage-service/storage.service.ts | 5 +- .../props/props.component.spec.ts | 2 +- .../props/props.component.ts | 60 +++++++-- ...esource-detail-html-content.component.html | 1 + ...urce-detail-html-content.component.spec.ts | 3 + .../resource-detail-html-content.component.ts | 26 ++++ .../resource-detail-html.component.html | 1 + .../resource-detail-html.component.spec.ts | 3 + .../resource-detail-html.component.ts | 26 ++++ .../resource-detail.component.html | 1 + .../resource-detail.component.spec.ts | 3 + .../resource-detail.component.ts | 39 +++++- 15 files changed, 267 insertions(+), 43 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7e10d678b3..81967fe553 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -25,15 +25,6 @@ export class AppComponent { constructor(private readonly router: Router, private routerEventsService: RouterEventsService) { this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { - /* - // set current route to sessionStorage - this.storageService.setStorageKey( - StorageType.sessionStorage, - AppConfig.AWG_APP_ROUTE_STORAGE_KEY, - event.urlAfterRedirects - ); - */ - // catch GoogleAnalytics pageview events, // cf. https://codeburst.io/using-google-analytics-with-angular-25c93bffaa18 (window as any).ga('set', 'page', event.urlAfterRedirects); diff --git a/src/app/core/services/conversion-service/conversion.service.ts b/src/app/core/services/conversion-service/conversion.service.ts index 52472259f7..4107a7543f 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -6,7 +6,6 @@ import { Observable } from 'rxjs'; import { NgxGalleryImage } from '@kolkov/ngx-gallery'; import { ApiService } from '@awg-core/services/api-service'; -import { StorageService, StorageType } from '@awg-core/services/storage-service'; import { GeoNames } from '@awg-core/core-models'; import { @@ -24,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, @@ -80,9 +78,8 @@ export class ConversionService extends ApiService { * and a private {@link StorageService} instance. * * @param {HttpClient} http Instance of the HttpClient. - * @param {StorageService} storageService Instance of the StorageService. */ - constructor(public http: HttpClient, private storageService: StorageService) { + constructor(public http: HttpClient) { super(http); } diff --git a/src/app/core/services/gnd-service/gnd.service.ts b/src/app/core/services/gnd-service/gnd.service.ts index 2f2de20a13..7949a250b8 100644 --- a/src/app/core/services/gnd-service/gnd.service.ts +++ b/src/app/core/services/gnd-service/gnd.service.ts @@ -2,39 +2,131 @@ 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 { + /** + * Readonly variable: gndKey. + * + * It holds the public key that is set to the storage. + */ readonly gndKey = 'gnd'; - constructor(private storageService: StorageService) {} - - writeGndToSessionStorage(value: string): void { - const dnbReg = /href="(https?:\/\/d-nb.info\/gnd\/(\w{5,10}))"/i; // regexp for links - let linkRegArr: RegExpExecArray; + /** + * 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 - let gndItem: string; + /** + * Public variable: linkRegArr. + * + * It holds the result array of a regex check execution . + */ + linkRegArr: RegExpExecArray; - const valueHasGnd = (checkValue: string): boolean => { - // check for dnbLink in value - if (dnbReg.exec(checkValue)) { - linkRegArr = dnbReg.exec(checkValue); - return !!linkRegArr; - } - return false; - }; + constructor(private storageService: StorageService) {} - if (valueHasGnd(value)) { - // split utf8str with gnd value into array and take last argument (pop) - gndItem = linkRegArr.pop(); + /** + * 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(); this.storageService.setStorageKey(StorageType.sessionStorage, this.gndKey, gndItem); } 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.storageService.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.exec(checkValue)) { + this.linkRegArr = this.dnbReg.exec(checkValue); + } else { + this.linkRegArr = null; + } + return !!this.linkRegArr; + } } diff --git a/src/app/core/services/storage-service/storage.service.ts b/src/app/core/services/storage-service/storage.service.ts index 4551aa904f..50c27e9636 100644 --- a/src/app/core/services/storage-service/storage.service.ts +++ b/src/app/core/services/storage-service/storage.service.ts @@ -132,16 +132,17 @@ export class StorageService { * * @param {Storage} storage The given storage type. * - * @returns {boolean} The local reference to Storage for 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 = JSON.stringify(new 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 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 1c7cb5155a..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,7 +1,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { GndService } from '@awg-core/services'; +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. @@ -24,6 +25,14 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit, OnDestro @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. * @@ -40,8 +49,6 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit, OnDestro */ metaBreakLine = 'Versionsdatum'; - constructor(private gndService: GndService) {} - /** * Angular life cycle hook: ngOnInit. * @@ -68,19 +75,56 @@ export class ResourceDetailHtmlContentPropsComponent implements OnInit, OnDestro this.resourceRequest.emit(id); } - setLabel(prop) { - if (!prop) { + /** + * 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[0]) { - prop.values.map(value => this.gndService.writeGndToSessionStorage(value)); + 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 - this.gndService.removeGndFromSessionStorage(); + 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. * From 80648c5204a7bc0a46455491bff5de23497978f1 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Fri, 7 Feb 2020 15:03:38 +0100 Subject: [PATCH 12/30] feat(core): expose GND via postMessage to communicate with inseri Relates to nie-ine/inseri#388 --- src/app/core/services/gnd-service/gnd.service.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/core/services/gnd-service/gnd.service.ts b/src/app/core/services/gnd-service/gnd.service.ts index 7949a250b8..160b3d4679 100644 --- a/src/app/core/services/gnd-service/gnd.service.ts +++ b/src/app/core/services/gnd-service/gnd.service.ts @@ -54,7 +54,7 @@ export class GndEvent { @Injectable({ providedIn: 'root' }) -export class GndService { +export class GndService extends StorageService { /** * Readonly variable: gndKey. * @@ -76,8 +76,6 @@ export class GndService { */ linkRegArr: RegExpExecArray; - constructor(private storageService: StorageService) {} - /** * Public method: setGndToSessionStorage. * @@ -93,7 +91,12 @@ export class GndService { let gndItem: string; // take last argument (pop) of linkRegArray gndItem = this.linkRegArr.pop().toString(); - this.storageService.setStorageKey(StorageType.sessionStorage, this.gndKey, gndItem); + + // 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(); } @@ -108,7 +111,7 @@ export class GndService { * @returns {void} It removes the key/value pair from the storage. */ removeGndFromSessionStorage(): void { - this.storageService.removeStorageKey(StorageType.sessionStorage, this.gndKey); + this.removeStorageKey(StorageType.sessionStorage, this.gndKey); } /** From 29d1c34b7e31b0135982a9061085e75b1d781b90 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Fri, 7 Feb 2020 15:32:01 +0100 Subject: [PATCH 13/30] fix(edition): add missing content description of op. 25 --- .../data/edition/series1/section5/op25/source-description.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/assets/data/edition/series1/section5/op25/source-description.json b/src/assets/data/edition/series1/section5/op25/source-description.json index 889393445b..57ac5a8e81 100644 --- a/src/assets/data/edition/series1/section5/op25/source-description.json +++ b/src/assets/data/edition/series1/section5/op25/source-description.json @@ -14,8 +14,7 @@ "Taktzahlen: Taktzahlen 1–2, 3–6 und 9–15 in M 317 Sk4 sowie 2–3 in M 317 Sk4.1 mit schwarzem Buntstift; Taktzahlen 7–8 in M 317 Sk4 mit Bleistift und nachgezogen mit schwarzem Buntstift.
      Taktzahl 52 (zu op. 24/1) auf Bl. 39r oben links mit schwarzem Buntstift.", "Instrumentenvorsatz: Akkoladenstrich und teilweise Schlüsselvorsatz auf Bl. 38v System 1–4 (zu op. 24/1).
      Akkoladenstrich und Schlüsselvorsatz auf Bl. 39r System 1–4 (zu op. 24/1), eingekreist und gestrichen mit rotem Buntstift.", "Eintragungen: Reihe auf Bl. 38v System 5 links (zu M 317 Sk3.1.3) mit schwarzem Buntstift.", - "Inhalt:
      Bl. 38v System 1b: T. 1–2: Skizze zu Nr. I T. 1–2 (M317 SkI/1).
      Bl. 38v System 1a: Reihenfragment (Teil A); Zwölftonreihe Gcis (Teil B) (SkI/2).
      ...", - "[Weitere Inhaltsangaben folgen in Kürze (02/2020).]" + "Inhalt:
      Bl. 38v System 1b: T. 1–2: Skizze zu Nr. I T. 1–2 (M 317 SkI/1).
      Bl. 38v System 1a: Reihenfragment (Teil A); Zwölftonreihe Gcis (Teil B) (M 317 SkI/2).
      Bl. 38v System 1c–3c: T. 1–4 (M 317 SkI/2.1).
      Bl. 38v System 1d: T. 1–2 (M 317 SkI/2.1.1).
      Bl. 38v System 2a: T. 3 (M 317 SkI/2.1.2).
      Bl. 38v System 4c: T. 3 (M 317 SkI/2.1.2.1).
      Bl. 38v System 3d–4d: Tonbuchstaben (M 317 SkI/2.1.3).
      Bl. 38v System 3a: T. 1–5 (M 317 SkI/3).
      Bl. 38v System 3b–5b: T. 2–4, {{ '{' }}5–6{{ '}' }} (M 317 SkI/3.1).
      Bl. 38v System 5c–6b: T. 4–5 (M 317 SkI/3.1.1).
      Bl. 38v System 4a: T. 3–5 (M 317 SkI/3.1.2).
      Bl. 38v System 4a: T. 3–5 (M 317 SkI/3.1.2).
      M 317 SkI/3.1.3 (Reihentabelle):
      Bl. 38v System 6a: Gg (1); Bl. 38v System 6b: Kgis (2)
      Bl. 38v System 7a: Ug (3); Bl. 38v System 7b: KUfis (4)
      Bl. 38v System 6c: Zwölftonreihe Gg (M 317 SkI/3.1.3.1).
      Bl. 38v System 10–12 T. {{ '{' }}1A{{ '}' }}, {{ '{' }}1B{{ '}' }},
      T. 1–2, 3A, (M 317 SkI/4a),
      Bl. 39r System 1–8b: T. 3B, 4–6 (M 317 SkI/4b),
      Bl. 39r System 9–12, 13c, 14b: T. 7–12 (M 317 SkI/4c),
      Bl. 39r System 13a–16a: T. 13–15 (M 317 SkI/4d).
      Bl. 38v System 13–15: T. 2,{{ '{' }}3{{ '}' }} (M 317 SkI/4.1).
      Bl. 39r System 13b: T. 13 (M 317 SkI/4.2).
      Bl. 39r System 15c–16c: T. 13 (M 317 SkI/4.3)." ] }, { From e97cd989373830c5c045177ce30c00497278e8d9 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Fri, 7 Feb 2020 15:34:03 +0100 Subject: [PATCH 14/30] style(edition): fix linebreaks of source E in op25 --- src/assets/data/edition/series1/section5/op25/source-list.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/data/edition/series1/section5/op25/source-list.json b/src/assets/data/edition/series1/section5/op25/source-list.json index 5bbadd5374..47dcbe167c 100644 --- a/src/assets/data/edition/series1/section5/op25/source-list.json +++ b/src/assets/data/edition/series1/section5/op25/source-list.json @@ -26,7 +26,7 @@ }, { "siglum": "E", - "type": "Autograph von Drei Lieder nach Gedichten von Hildegard Jone op. 25:\n I (\"Wie bin ich froh!\" M 317: Textfassung [1-->] 2)\n II (\"Des Herzens Purpurvogel\" M 322: einzige Textfassung)\n III (\"Sterne, Ihr silbernen Bienen\" M 321: einzige Textfassung).", + "type": "Autograph von Drei Lieder nach Gedichten von Hildegard Jone op. 25:
      I (\"Wie bin ich froh!\" M 317: Textfassung [1-->] 2)
      II (\"Des Herzens Purpurvogel\" M 322: einzige Textfassung)
      III (\"Sterne, Ihr silbernen Bienen\" M 321: einzige Textfassung).", "location": "Wien, Universal Edition, UEQ 493.", "linkTo": "op25_sourceNotA" } From b55fd711f1a78c591ab2d29a5d31fdc21505d573 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sat, 8 Feb 2020 02:11:45 +0100 Subject: [PATCH 15/30] test(core): add and improve tests for gnd & storage services --- .../services/gnd-service/gnd.service.spec.ts | 266 +++++++++++++++++- .../core/services/gnd-service/gnd.service.ts | 2 +- .../storage-service/storage.service.spec.ts | 64 ++++- 3 files changed, 317 insertions(+), 15 deletions(-) diff --git a/src/app/core/services/gnd-service/gnd.service.spec.ts b/src/app/core/services/gnd-service/gnd.service.spec.ts index f3610bac14..176d1cc641 100644 --- a/src/app/core/services/gnd-service/gnd.service.spec.ts +++ b/src/app/core/services/gnd-service/gnd.service.spec.ts @@ -1,12 +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', () => { - beforeEach(() => TestBed.configureTestingModule({})); + 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', () => { - const service: GndService = TestBed.get(GndService); - expect(service).toBeTruthy(); + 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 index 160b3d4679..cff874330d 100644 --- a/src/app/core/services/gnd-service/gnd.service.ts +++ b/src/app/core/services/gnd-service/gnd.service.ts @@ -125,7 +125,7 @@ export class GndService extends StorageService { * @return {boolean} The boolean result of the check. */ private valueHasGnd(checkValue: string): boolean { - if (this.dnbReg.exec(checkValue)) { + if (this.dnbReg.test(checkValue)) { this.linkRegArr = this.dnbReg.exec(checkValue); } else { this.linkRegArr = null; diff --git a/src/app/core/services/storage-service/storage.service.spec.ts b/src/app/core/services/storage-service/storage.service.spec.ts index 188359449c..ea75a130e1 100644 --- a/src/app/core/services/storage-service/storage.service.spec.ts +++ b/src/app/core/services/storage-service/storage.service.spec.ts @@ -1,11 +1,6 @@ import { TestBed } from '@angular/core/testing'; -import Spy = jasmine.Spy; - -import { expectSpyCall } from '@testing/expect-helper'; - import { StorageService, StorageType } from './storage.service'; -import { throwError } from 'rxjs'; describe('StorageService', () => { let storageService: StorageService; @@ -111,12 +106,15 @@ describe('StorageService', () => { 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); @@ -133,6 +131,7 @@ describe('StorageService', () => { }); 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}`); @@ -142,16 +141,19 @@ describe('StorageService', () => { 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); @@ -159,6 +161,7 @@ describe('StorageService', () => { }); it(`- storage is not supported`, () => { + expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); spyOn(storageService, 'storageIsSupported').and.returnValue(false); storageService.setStorageKey(sessionType, expectedKey, expectedItem); @@ -166,21 +169,25 @@ describe('StorageService', () => { }); 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(); }); @@ -189,7 +196,9 @@ describe('StorageService', () => { 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}` @@ -199,6 +208,9 @@ describe('StorageService', () => { 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); @@ -209,38 +221,48 @@ describe('StorageService', () => { expect(storageService.getStorageKey(localType, expectedKey)).toEqual(otherItem, `should be ${otherItem}`); }); - it('... should return null for non existing items', () => { - expect(expectedMockStorage.getItem(expectedKey)).toBeNull(); // null - expect(storageService.getStorageKey(sessionType, 'key')).toBeNull(); + 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(); @@ -250,6 +272,7 @@ describe('StorageService', () => { 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}`); @@ -259,6 +282,9 @@ describe('StorageService', () => { 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); @@ -273,48 +299,64 @@ describe('StorageService', () => { }); 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); - spyOn(storageService, 'storageHasKey').and.returnValue(false); + 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); - spyOn(storageService, 'storageIsSupported').and.returnValue(undefined); + 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); - spyOn(storageService, 'storageIsAvailable').and.returnValue(undefined); + 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}`); From 3dabd8af6b438088b7a87c65b2134b9e39f58dbe Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sun, 9 Feb 2020 00:04:44 +0100 Subject: [PATCH 16/30] feat(edition): add almost complete TkA for op25 (Sk4 incomplete) --- .../edition-detail.component.ts | 3 +- .../edition-tka-table.component.html | 8 +- .../textcritics/textcritics.component.html | 20 +- .../textcritics/textcritics.component.ts | 10 +- .../edition-view/models/textcritics.model.ts | 2 +- .../series1/section5/op12/textcritics.json | 2 +- .../series1/section5/op25/textcritics.json | 439 +++++++++++++++++- 7 files changed, 455 insertions(+), 29 deletions(-) diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts index 45d1109fd3..770ae91d0d 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts @@ -8,7 +8,6 @@ import { EditionSvgSheet, EditionSvgSheetList, EditionWork, - EditionWorks, FolioConvoluteList, TextcriticalComment, TextcriticsList @@ -136,7 +135,7 @@ export class EditionDetailComponent implements OnInit { // get current editionWork from editionService .getEditionWork() .pipe( - switchMap(work => { + switchMap((work: EditionWork) => { // set current editionWork this.editionWork = work; // return EditionDetailData from editionDataService diff --git a/src/app/views/edition-view/edition-outlets/edition-tka-table/edition-tka-table.component.html b/src/app/views/edition-view/edition-outlets/edition-tka-table/edition-tka-table.component.html index d08d76327c..6ae99abb84 100644 --- a/src/app/views/edition-view/edition-outlets/edition-tka-table/edition-tka-table.component.html +++ b/src/app/views/edition-view/edition-outlets/edition-tka-table/edition-tka-table.component.html @@ -2,16 +2,16 @@ - - + + - - + + diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html index 44348f1185..991f0c70d9 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html @@ -6,21 +6,23 @@
      {{ textcritic.id }}
      -
      +
      +

      Skizzenkommentar:

      - - - +
      +

      Textkritischer Kommentar:

      + + +
      diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts index b1ea27f768..58a578a91d 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts @@ -90,15 +90,15 @@ export class TextcriticsComponent implements OnInit { } /** - * Public method: hasComments. + * Public method: isNotEmptyArray. * - * It checks if a given textcritical comment array + * It checks if a given array of the textcritical comment input * is not empty. * - * @param {string} comments The given TextCriticalComment array. + * @param {TextcriticalComment[] | string[]} part The given array input. * @returns {boolean} The boolean result of the check. */ - hasComments(comments: TextcriticalComment[]): boolean { - return comments && comments.constructor === Array && comments.length > 0; + isNotEmptyArray(checkArray: TextcriticalComment[] | string[]): boolean { + return checkArray && checkArray.constructor === Array && checkArray.length > 0; } } diff --git a/src/app/views/edition-view/models/textcritics.model.ts b/src/app/views/edition-view/models/textcritics.model.ts index 202b1c4129..538a6373af 100644 --- a/src/app/views/edition-view/models/textcritics.model.ts +++ b/src/app/views/edition-view/models/textcritics.model.ts @@ -43,7 +43,7 @@ export class Textcritics { /** * The description of the textcritics. */ - description: string; + description: string[]; /** * The array of textcritical comments from a textcritics list. diff --git a/src/assets/data/edition/series1/section5/op12/textcritics.json b/src/assets/data/edition/series1/section5/op12/textcritics.json index 0a2d4acc7f..494f8ae989 100644 --- a/src/assets/data/edition/series1/section5/op12/textcritics.json +++ b/src/assets/data/edition/series1/section5/op12/textcritics.json @@ -3,7 +3,7 @@ { "id": "Aa:SkI/1", "description": [ - "[Der Textkritische Kommentar zu Aa:Sk1/1 erscheint im Zusammenhang der vollständigen Edition der Vier Lieder op. 12 in AWG I/5.]" + "[Der Skizzenkommentar und die textkritischen Kommentare zu Aa:Sk1/1 erscheinen im Zusammenhang der vollständigen Edition der Vier Lieder op. 12 in AWG I/5.]" ], "comments": [] }, diff --git a/src/assets/data/edition/series1/section5/op25/textcritics.json b/src/assets/data/edition/series1/section5/op25/textcritics.json index 9e1017f598..bea2a16b9b 100644 --- a/src/assets/data/edition/series1/section5/op25/textcritics.json +++ b/src/assets/data/edition/series1/section5/op25/textcritics.json @@ -1,32 +1,457 @@ { "textcritics": [ { - "id": "A:Sk1", + "id": "A:SkI/1", "description": [ - "[Der Textkritische Kommentar zu A:Sk1 erscheint im Zusammenhang der vollständigen Edition der Drei Lieder nach Gedichten von Hildegard Jone op. 25 in AWG I/5.]" + "Sk1 entwirft eine viertönige Figur, die in ihrem Tonvorrat und ihrer rhythmischen Kontur dem Anfang der Singstimme von „Wie bin ich froh!“ M 317 entspricht. Dass es sich um die erste Skizzeneinheit zu dem Lied handelt, erschließt sich aus dem skripturalen Zusammenhang mit der links danebenstehenden Einheit Sk2: Diese weist an ihrem rechten Ende ein deutlich zusammengedrängtes Schriftbild auf, was sich durch den bereits von Sk1 beanspruchten Raum auf der Seite erklärt.
      Noch vor der Niederschrift von Sk1 war die Seite am linken Rand vermutlich mit dem Akkoladenstrich von System 1–4 sowie der Schlüsselung in System 2–3 versehen: Diese präparieren die Seite für die Fortsetzung der (bereits im Sommer 1931 begonnenen) Verlaufskizze zu op. 24/I auf Bl. 22v–26r, 31v–33r, und 36v–38r im Skizzenbuch. (Siehe auch Akkoladenstrich, Schlüsselung und Taktziffer am linken Rand von Bl. 39r System 1–4, die ebenfalls als Präparierung der Verlaufskizze zu op. 24/1 fungieren [Sk4 T. 3B–6].)
      Die Datierung 4. VII. 34 oberhalb vom linken Rand von System 1 ist möglicherweise bereits im Zusammenhang mit Sk1, vielleicht aber auch erst mit Sk2 notiert worden.
      Der über System 1 stehende Titel „Wie bin ich froh!“ ist allerdings sehr wahrscheinlich erst nach Abschluss der Verlaufskizze Sk4, möglicherweise erst bei einer späteren Redaktion des gesamten Skizzenbuches eingetragen worden, worauf der auch für andere entsprechende Titel im Skizzenbuch verwendete Schreibstoff und Schreibduktus hinweist." + ], + "comments": [ + { + "measure": "1–2", + "system": "1", + "position": "", + "comment": "Durch Einklammerung rechts getilgt." + } + ] + }, + { + "id": "A:SkI/2", + "description": [ + "Sk2 entwirft eine Zwölftonreihe in zwei Anläufen. Reihe A umfasst lediglich fünf Töne, Reihe B ergänzt nach einer Permutation des 3.–4. Reihentons die Reihe zur vollständigen Zwölftönigkeit. Die ersten fünf Töne beider Reihen ergänzen den ungeordneten Tonvorrat von Sk1 um den Anfangston cis. Die Niederschrift von Reihe B erfolgt dann im dynamischen Abgleich mit Sk2.1, Sk2.1.1, Sk2.1.2, Sk2.1.2.1 und Sk2.1.3." + ], + "comments": [ + { + "measure": "A", + "system": "1", + "position": "1.–5. Reihenton", + "comment": "Durch Einklammerung rechts getilgt." + }, + { + "measure": "A", + "system": "1", + "position": "2. Reihenton", + "comment": "♮ nachgezogen (überschreibt ♭?)." + }, + { + "measure": "B", + "system": "1", + "position": "4. Reihenton", + "comment": "♯fis2 überschreibt ♮g2." + } + ] + }, + { + "id": "A:SkI/2.1", + "description": [ + "Sk2.1 entwirft auf der Grundlage der rhythmischen Kontur von Sk1 eine Melodielinie mit Text, die in ihrer ausführlichsten und vermutlich letzten Schicht (T. 1 bis T. 2 1. TH: System 1; T. 2 2. TH bis T. 4: System 2) Reihe B aus Sk2 artikuliert. Die Niederschrift erfolgt im dynamischen Abgleich mit Sk2.1.1, Sk2.1.2, Sk2.1.2.1 und Sk2.1.3." + ], + "comments": [ + { + "measure": "1–3", + "system": "1", + "position": "", + "comment": "Text in Kurrentschrift." + }, + { + "measure": "1", + "system": "1", + "position": "6/8", + "comment": "♭ nachgezogen (überschreibt Ansatz von ♯ zu dis2?)." + }, + { + "measure": "2", + "system": "1", + "position": "1. Note", + "comment": "Durch Einkreisung getilgt." + }, + { + "measure": "2", + "system": "1", + "position": "3/8", + "comment": "♯ nachgezogen (überschreibt ♮?)." + }, + { + "measure": "2", + "system": "3", + "position": "letzte Note", + "comment": "Punkt unter der Note: Fleck oder Ansatz zu Notenkopf f?" + }, + { + "measure": "2
      bis 3", + "system": "3", + "position": "letzte Note
      3 . Note", + "comment": "Durch Einklammerung getilgt." + }, + { + "measure": "3", + "system": "1", + "position": "3 . Note", + "comment": "Klammerung rechts zeigt Tilgung an. Es ist nicht eindeutig, ob die so angezeigte Tilgung ab T. 2 4/8 beginnt (mit den Varianten in System 2–3) oder ob sie sich auch auf T. 1 ff. bezieht (mit den Varianten in Sk2.1.1 und Sk2.1.2." + }, + { + "measure": "3", + "system": "2", + "position": "4/8", + "comment": "Punktierte Achtelnote c1 überschreibt radierte Achtelpause." + } + ] + }, + { + "id": "A:SkI/2.1.1", + "description": [ + "Sk2.1.1 formuliert eine Variante des Anfangs von Sk2.1 auf der Grundlage des 1.–5. Reihentons von Reihe B in Sk2.", + "[Zu dieser Skizze gibt es keine textkritischen Kommentare.]" ], "comments": [] }, { - "id": "A:Sk2", + "id": "A:SkI/2.1.2", + "description": [ + "Sk2.1.2 ist vermutlich eine inhaltliche Fortsetzung von Sk2.1.1 (als T. 3): Hierfür spricht der Anfangston b1 an entsprechender Stelle von Varianten in Sk2.1 (System 1 und 2) sowie die dort ebenfalls anzutreffende Ligatur über den Taktstrich (System 1 und 3). Allerdings artikuliert Sk2.1.2 einen anderen Reihenverlauf als Reihe B in Sk2." + ], + "comments": [ + { + "measure": "3", + "system": "2", + "position": "", + "comment": "Durch Einklammerung getilgt." + } + ] + }, + { + "id": "A:SkI/2.1.2.1", "description": [ - "[Der Textkritische Kommentar zu A:Sk2 erscheint im Zusammenhang der vollständigen Edition der Drei Lieder nach Gedichten von Hildegard Jone op. 25 in AWG I/5.]" + "Sk2.1.2.1 formuliert eine rhythmisch-metrische Variante des Anfangs von Sk2.1.2.", + "[Zu dieser Skizze gibt es keine textkritischen Kommentare.]" ], "comments": [] }, { - "id": "A:Sk3", + "id": "A:SkI/2.1.3", "description": [ - "[Der Textkritische Kommentar zu A:Sk3 erscheint im Zusammenhang der vollständigen Edition der Drei Lieder nach Gedichten von Hildegard Jone op. 25 in AWG I/5.]" + "Die in Sk2.1.3 notierten Tonbuchstaben c, f, gis und a beziehen sich vermutlich auf noch fehlende Reihentöne bei der Niederschrift von Sk2 (Reihe B), Sk2.1 und Sk2.1.2.", + "[Zu dieser Skizze gibt es keine textkritischen Kommentare.]" + ], + "comments": [] + }, + { + "id": "A:SkI/3", + "description": [ + "Sk3 stellt gegenüber Sk2 und deren begleitenden Skizzeneinheiten einen inhaltlichen Neuansatz dar, insofern die in Sk2 notierte vollständige Zwölftonreihe (Reihe B) nicht mehr verwendet wird. Stattdessen formuliert Sk3 auf der Grundlage des permutierten Tonvorrats von Sk1 einen neuen zwölftönigen Verlauf. Im dynamischen Abgleich mit Sk3.1, Sk3.1.1, Sk3.1.2 wird dieser Verlauf weiter verändert und als rhythmisch abstrahierte Zwölftonreihe in Sk3.1.3 und Sk3.1.3.1 notiert." + ], + "comments": [ + { + "measure": "1
      bis 2", + "system": "3", + "position": "1. Note
      1. Note", + "comment": "Text in Kurrentschrift." + }, + { + "measure": "2", + "system": "3", + "position": "1. Note", + "comment": "Punktierung durch Einkreisung getilgt?" + }, + { + "measure": "2", + "system": "3", + "position": "1. Note", + "comment": "Pfeil zur Variante der Fortsetzung in Sk3.1." + } + ] + }, + { + "id": "A:SkI/3.1", + "description": [ + "Sk3.1 führt den Anfang von Sk3 ab dem Auftakt zu T. 3 in Varianten weiter bis T. {{ '{' }}6{{ '}' }}." + ], + "comments": [ + { + "measure": "vor 3
      bis 3", + "system": "4", + "position": "1. Note
      1. Note", + "comment": "Text in Kurrentschrift." + }, + { + "measure": "3", + "system": "4", + "position": "Taktanfang", + "comment": "Asterisk verweist auf Variante der Fortsetzung in Sk3.1.2." + }, + { + "measure": "{{ '{' }}5–6{{ '}' }}", + "system": "4", + "position": "", + "comment": "Durch Einklammerung getilgt. (Die Variante in System 3 wird allerdings nicht von der Einklammerung eingeschlossen.)" + }, + { + "measure": "{{ '{' }}5{{ '}' }}", + "system": "4", + "position": "1. Note", + "comment": "Pfeil unten rechts von Variante T. 4 in Sk3.1.1.
      Ligaturbogen gestrichen." + }, + { + "measure": "{{ '{' }}5{{ '}' }}", + "system": "4", + "position": "6/8", + "comment": "I markiert hier als Reihenziffer vermutlich den wiederholten Beginn der in Sk3 T. 1–2 1. Note und in Sk3.1 fortgeführten Zwölftonreihe. Diese Reihe ist nicht identisch mit Gg in den Reihentabellen Sk3.1.3 und M 317 Sk5 / M 321 Sk1 / M 322 Sk1 in C (dort jeweils mit arabischer Ziffer 1 bezeichnet)." + }, + { + "measure": "{{ '{' }}5{{ '}' }}
      bis {{ '{' }}6{{ '}' }}", + "system": "4", + "position": "6/8
      3. Note", + "comment": "Text in Kurrentschrift." + }, + { + "measure": "{{ '{' }}6{{ '}' }}", + "system": "4", + "position": "1–2/8", + "comment": "Text: leuch[-]tet zusammengeschrieben." + } + ] + }, + { + "id": "A:SkI/3.1.1", + "description": ["Sk3.1.1 formuliert Varianten von T. 4–5 in Sk3.1."], + "comments": [ + { + "measure": "5", + "system": "6", + "position": "", + "comment": "Halbe Note überschreibt Viertelnote." + } + ] + }, + { + "id": "A:SkI/3.1.2", + "description": [ + "Sk3.1.2 formuliert eine Variante von Sk3.1 ab T. 3. Zusammen mit Sk3 T. 1–2 1. Note und Sk3.1 Auftakt zu T. 3 ergibt sich eine Tonfolge, die der Reihenform Gg (1) der Zwölftonreihe in Sk3.1.3 und Sk3.1.3.1 entspricht." + ], + "comments": [ + { + "measure": "3
      bis 5", + "system": "4", + "position": "1. Note
      1. Note", + "comment": "Text in Kurrentschrift." + }, + { + "measure": "3", + "system": "4", + "position": "Taktanfang", + "comment": "Asterisk verweist auf Anschluss von Sk3.1." + }, + { + "measure": "3", + "system": "4", + "position": "1. Note", + "comment": "Punktierung durch Einkreisung getilgt." + }, + { + "measure": "5", + "system": "4", + "position": "1. Note", + "comment": "Punktierte Viertelnote überschreibt Halbe Note." + }, + { + "measure": "5", + "system": "4", + "position": "nach 1. Note", + "comment": "Durch Einklammerung abgegrenzt von Sk3.1." + } + ] + }, + { + "id": "A:SkI/3.1.3", + "description": [ + "Sk3.1.3 notiert die vier in op. 25/I verwendeten Reihenformen in einer tabellarischen Anordnung und versieht sie mit den ebenfalls in Sk4 und Sk4.1 verwendeten Reihenziffern 1 bis 4. Die Permutation des 10.–11. Reihentons von Gg und Ug bzw. des 2.–3. Reihentons von Kgis und KUfis ist bereits in Sk3.1.2 vollzogen. " + ], + "comments": [ + { + "measure": "", + "system": "5", + "position": "", + "comment": "Strich mit unklarer Bedeutung." + }, + { + "measure": "Gg (1)", + "system": "6", + "position": "vor 1. Reihenton", + "comment": "1 mit rotem Buntstift." + }, + { + "measure": "Gg (1)", + "system": "6", + "position": "7. Reihenton", + "comment": "sic: kein ♮." + }, + { + "measure": "Gg (1)", + "system": "6", + "position": "10.–11. Reihenton", + "comment": "♮a, ♮c1 durch Einkreisung getilgt und ersetzt durch c2 (sic: kein ♮), ♮a1." + }, + { + "measure": "Kgis (2)", + "system": "6", + "position": "vor 1. Reihenton", + "comment": "2 mit grünem Buntstift." + }, + { + "measure": "Kgis (2)", + "system": "6", + "position": "2.–3. Reihenton", + "comment": "♮c1, ♮a durch Einkreisung getilgt und ersetzt durch a1, c2 (sic: keine ♮)." + }, + { + "measure": "Ug (3)", + "system": "7", + "position": "vor 1. Reihenton", + "comment": "3 mit schwarzem Buntstift." + }, + { + "measure": "Ug (3)", + "system": "7", + "position": "10.–11. Reihenton", + "comment": "♮f2, ♮d2 durch Einkreisung getilgt und ersetzt durch ♮d1, ♮f1." + }, + { + "measure": "KUfis (4)", + "system": "7", + "position": "vor 1. Reihenton", + "comment": "4 mit rotem Buntstift." + }, + { + "measure": "KUfis (4)", + "system": "7", + "position": "2.–3. Note", + "comment": "♮d2, ♮f2 durch Einkreisung getilgt und ersetzt durch ♮f1, d1 (sic: kein ♮)." + }, + { + "measure": "", + "system": "6–7", + "position": "nach letzter Note", + "comment": "Klammer rechts mit rotem Buntstift." + } + ] + }, + { + "id": "A:SkI/3.1.3.1", + "description": [ + "Sk3.1.3.1 notiert das Ergebnis der in Sk3.1.3 erkennbaren Änderung des 10.–11. Reihentons von Gg und Ug bzw. des 2.–3. Reihentons von Kgis und KUfis am Beispiel von Gg noch einmal in einem einheitlichen Oktavregister.", + "[Zu dieser Skizze gibt es keine textkritischen Kommentare.]" ], "comments": [] }, { "id": "A:Sk4", "description": [ - "[Der Textkritische Kommentar zu A:Sk4 erscheint im Zusammenhang der vollständigen Edition der Drei Lieder nach Gedichten von Hildegard Jone op. 25 in AWG I/5.]" + "Sk4 ist eine vollständige Verlaufskizze des Liedes „Wie bin ich froh!“. Die Verlaufskizze erstreckt sich über vier Akkoladen (T. {{ '{' }}1A{{ '}' }}–3A, T. 3B–6, T. 7–12 und T. 13–15) und umfasst im Unterschied zu den Reinschriften von Textfassung 1 und Textfassung 2 in B und E nicht zwölf, sondern 15 gültige Takte.
      Bereits vor der Niederschrift von Sk4 war Bl. 39r mit Akkoladenstrich, Schlüsselung und Taktziffer am linken Rand von System 1–4 versehen (Sk4 T. 3B–6): Diese präparieren die Seite für die Fortsetzung der (bereits im Sommer 1931 begonnenen) Verlaufskizze zu op. 24/I auf Bl. 22v–26r, 31v–33r, und 36v–38r im Skizzenbuch.
      (Siehe auch Akkoladenstrich von System 1–4 sowie die Schlüsselung in System 2–3 am linken Rand von Bl. 38v, die ebenfalls als Präparierung der Verlaufskizze zu op. 24/I fungieren [Sk1 bis Sk3.1.2].)
      Sk 4 ist laut der nach T. 15 folgenden Datierung am 16. Juli 1934 beendet worden.", + "Die Ziffern 1 (Gg), 2 (Kgis), 3 (Ug) und 4 (KUfis) bezeichnen in der Regel den Anfang von Reihenformen gemäß den Reihentabellen Sk3.1.3 und M 317 Sk5 / M 321 Sk1 / M 322 Sk1 in C. Winkel markieren in der Regel deren Ende (┐, ┘) oder Anfang (┌, └)." + ], + "comments": [ + { + "measure": "1A", + "system": "10–12", + "position": "", + "comment": "Durch Einklammerung rechts getilgt." + }, + { + "measure": "1B", + "system": "10–12", + "position": "", + "comment": "Durch Einklammerung rechts getilgt." + }, + { + "measure": "vor 1C", + "system": "11", + "position": "1. Note", + "comment": "4 mit rotem Buntstift." + }, + { + "measure": "1C", + "system": "11", + "position": "6/8", + "comment": "sic: Winkel markiert hier nicht das Ende, sondern den 11. Reihenton von KUfis (4). 12. Reihenton von KUfis (g) ist zugleich 1. Reihenton von Gg (1): siehe T. 2 System 10–11 jeweils 1. Note." + }, + { + "measure": "nach 1C", + "system": "11–12", + "position": "", + "comment": "Taktstrich nachgezogen mit rotem Buntstift." + }, + { + "measure": "2", + "system": "10", + "position": "1. Note", + "comment": "1 mit rotem Buntstift." + }, + { + "measure": "…", + "system": "…", + "position": "…", + "comment": "[Die vollständige Liste der textkritischen Kommentare zu Sk4 erscheint in Kürze (02/2020).]" + } + ] + }, + { + "id": "A:SkI/4.1", + "description": [ + "Sk4.1 formuliert eine rhythmisch-metrische Variante von Sk4 T. 2–3A. Der dort im Klavier offen gelassene Takt 3A wird in Sk4.1 ansatzweise gefüllt, aber dann insgesamt gestrichen (T. {{ '{' }}3{{ '}' }}): möglicherweise auch auf Grund des in skripturaler Hinsicht engen Raumes. Der Takt wird in Sk4 T. 3B weiter ausgearbeitet.
      Vor der Niederschrift von Sk4.1 war die Seite am linken Rand von System 14–16 mit einer Akkoladenklammer beschriftet, die offensichtlich einer Präparierung der Fortsetzung von Sk4 nach T. {{ '{' }}1A{{ '}' }}–3A dient. Nach der Niederschrift von Sk4.1 erfolgt die Fortsetzung von Sk4 jedoch auf Bl. 39r (Sk4 T. 3B–6)." + ], + "comments": [ + { + "measure": "2", + "system": "14", + "position": "1. Note", + "comment": "Beide Winkel und 1 (Gg) mit rotem Buntstift. " + }, + { + "measure": "2", + "system": "13", + "position": "6/8–4/4", + "comment": "Decrescendogabel zu 4/4 überschrieben zu Decrescendogabel von 6/8–4/4." + }, + { + "measure": "{{ '{' }}3{{ '}' }}", + "system": "", + "position": "", + "comment": "Durch Einklammerung links und Streichung getilgt. Streichung mit rotem Buntstift." + }, + { + "measure": "{{ '{' }}3{{ '}' }}", + "system": "14", + "position": "letzte Note", + "comment": "Winkel mit rotem Buntstift." + } + ] + }, + { + "id": "A:SkI/4.2", + "description": [ + "Sk4.2 formuliert vermutlich den Ansatz zu einer Variante der Singstimme in Sk4 T. 13.", + "[Zu dieser Skizze gibt es keine textkritischen Kommentare.]" ], "comments": [] + }, + { + "id": "A:SkI/4.3", + "description": [ + "Sk4.3 formuliert eine fragmentarische Variante von Sk4 T. 13." + ], + "comments": [ + { + "measure": "13", + "system": "16", + "position": "1. Note", + "comment": "sic: Verweisstrich führt zu Sk4 T. 14. Inhaltlich bezieht sich Sk4.3 jedoch auf Sk4 T. 13. " + }, + { + "measure": "13", + "system": "16", + "position": "letzte Note", + "comment": "Entzifferung von ♯ais unsicher: möglicherweise auch ♭b." + } + ] } ] } From 01ec0fd45db4f92e6e01e8364045c6a6df4bacce Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sun, 9 Feb 2020 02:23:06 +0100 Subject: [PATCH 17/30] feat(edition): make tka list toggleable per sketch --- .../edition-outlets/report/report.module.ts | 4 +- .../critics-list/critics-list.component.css | 0 .../critics-list/critics-list.component.html | 31 ++++++ .../critics-list.component.spec.ts | 42 ++++++++ .../critics-list/critics-list.component.ts | 96 +++++++++++++++++++ .../report/textcritics/critics-list/index.ts | 1 + .../report/textcritics/index.ts | 3 +- .../textcritics/textcritics.component.html | 30 ++---- .../textcritics/textcritics.component.spec.ts | 22 ++--- .../textcritics/textcritics.component.ts | 29 +----- 10 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.css create mode 100644 src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.html create mode 100644 src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.spec.ts create mode 100644 src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.ts create mode 100644 src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/index.ts diff --git a/src/app/views/edition-view/edition-outlets/report/report.module.ts b/src/app/views/edition-view/edition-outlets/report/report.module.ts index 836709dc43..ebdf83eadc 100644 --- a/src/app/views/edition-view/edition-outlets/report/report.module.ts +++ b/src/app/views/edition-view/edition-outlets/report/report.module.ts @@ -9,7 +9,7 @@ import { SourceEvaluationComponent, SourceListComponent } from './sources'; -import { TextcriticsComponent } from './textcritics'; +import { CriticsListComponent, TextcriticsComponent } from './textcritics'; /** * The report module. @@ -24,6 +24,7 @@ import { TextcriticsComponent } from './textcritics'; SourceDescriptionComponent, SourceEvaluationComponent, SourceListComponent, + CriticsListComponent, TextcriticsComponent ], exports: [ @@ -31,6 +32,7 @@ import { TextcriticsComponent } from './textcritics'; SourceDescriptionComponent, SourceEvaluationComponent, SourceListComponent, + CriticsListComponent, TextcriticsComponent ] }) diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.css b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.html b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.html new file mode 100644 index 0000000000..3d6d19dd1f --- /dev/null +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.html @@ -0,0 +1,31 @@ + + + +
      + + +
      +
      + +
      +

      Skizzenkommentar:

      +

      +
      +
      +

      Textkritischer Kommentar:

      + + +
      +
      +
      +
      diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.spec.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.spec.ts new file mode 100644 index 0000000000..53eec61c97 --- /dev/null +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.spec.ts @@ -0,0 +1,42 @@ +import { Component, Input } from '@angular/core'; + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CompileHtmlComponent } from '@awg-shared/compile-html'; +import { EditionTkaTableComponent } from '@awg-views/edition-view/edition-outlets/edition-tka-table/edition-tka-table.component'; +import { TextcriticalComment } from '@awg-views/edition-view/models'; + +import { CriticsListComponent } from './critics-list.component'; + +// mock tka table component +@Component({ selector: 'awg-edition-tka-table', template: '' }) +class EditionTkaTableStubComponent { + @Input() + textcriticalComments: TextcriticalComment[]; + + // TODO: handle outputs +} + +describe('CriticsListComponent', () => { + let component: CriticsListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [NgbAccordionModule], + declarations: [CriticsListComponent, CompileHtmlComponent, EditionTkaTableStubComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CriticsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.ts new file mode 100644 index 0000000000..9351cafd89 --- /dev/null +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/critics-list.component.ts @@ -0,0 +1,96 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { TextcriticalComment, TextcriticsList } from '@awg-views/edition-view/models'; + +@Component({ + selector: 'awg-critics-list', + templateUrl: './critics-list.component.html', + styleUrls: ['./critics-list.component.css'] +}) +export class CriticsListComponent implements OnInit { + /** + * Input variable: textcriticsData. + * + * It keeps the textcritics data. + */ + @Input() + textcriticsData: TextcriticsList; + + /** + * Output variable: openModalRequest. + * + * It keeps an event emitter to open the modal + * with the selected modal text snippet. + */ + @Output() + openModalRequest: EventEmitter = new EventEmitter(); + + /** + * Output variable: selectSvgSheetRequest. + * + * It keeps an event emitter for the selected id of an svg sheet. + */ + @Output() + selectSvgSheetRequest: EventEmitter = new EventEmitter(); + + /** + * Self-referring variable needed for CompileHtml library. + */ + ref: CriticsListComponent; + + /** + * Constructor of the TextcriticsComponent. + * + * It initializes the self-referring ref variable needed for CompileHtml library. + */ + constructor() { + this.ref = this; + } + + /** + * Angular life cycle hook: ngOnInit. + * + * It calls the containing methods + * when initializing the component. + */ + ngOnInit() {} + + /** + * Public method: openModal. + * + * It emits a given id of a modal snippet text + * to the {@link openModalRequest}. + * + * @param {string} id The given modal snippet id. + * @returns {void} Emits the id. + */ + openModal(id: string): void { + this.openModalRequest.emit(id); + } + + /** + * Public method: selectSvgSheet. + * + * It emits a given id of a selected svg sheet + * to the {@link selectSvgSheetRequest}. + * + * @param {string} id The given sheet id. + * @returns {void} Emits the id. + */ + selectSvgSheet(id: string): void { + this.selectSvgSheetRequest.emit(id); + } + + /** + * Public method: isNotEmptyArray. + * + * It checks if a given array of the textcritical comment input + * is not empty. + * + * @param {TextcriticalComment[] | string[]} part The given array input. + * @returns {boolean} The boolean result of the check. + */ + isNotEmptyArray(checkArray: TextcriticalComment[] | string[]): boolean { + return checkArray && checkArray.constructor === Array && checkArray.length > 0; + } +} diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/index.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/index.ts new file mode 100644 index 0000000000..d4dc12e55c --- /dev/null +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/critics-list/index.ts @@ -0,0 +1 @@ +export * from './critics-list.component'; diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/index.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/index.ts index 5f0dc9a95d..489e249049 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/index.ts +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/index.ts @@ -7,6 +7,7 @@ * */ +import { CriticsListComponent } from './critics-list'; import { TextcriticsComponent } from './textcritics.component'; -export { TextcriticsComponent }; +export { CriticsListComponent, TextcriticsComponent }; diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html index 991f0c70d9..3ae9bb6b37 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.html @@ -1,30 +1,12 @@ -
      -
      -
      - {{ textcritic.id }} -
      -
      -

      Skizzenkommentar:

      -

      -
      -
      -

      Textkritischer Kommentar:

      - - -
      -
      -
      +
      diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.spec.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.spec.ts index 3e1e26e648..8e0f170126 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.spec.ts +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.spec.ts @@ -2,20 +2,17 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, Input } from '@angular/core'; -import { RouterLinkStubDirective } from '@testing/router-stubs'; - import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; -import { CompileHtmlComponent } from '@awg-shared/compile-html'; -import { TextcriticalComment } from '@awg-views/edition-view/models'; - +import { TextcriticsList } from '@awg-views/edition-view/models'; +import { CriticsListComponent } from './critics-list/critics-list.component'; import { TextcriticsComponent } from './textcritics.component'; -// mock heading component -@Component({ selector: 'awg-edition-tka-table', template: '' }) -class EditionTkaTableStubComponent { +// mock critics list component +@Component({ selector: 'awg-critics-list', template: '' }) +class CriticsListStubComponent { @Input() - textcriticalComments: TextcriticalComment[]; + textcriticsData: TextcriticsList; // TODO: handle outputs } @@ -27,12 +24,7 @@ describe('TextcriticsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [NgbAccordionModule], - declarations: [ - TextcriticsComponent, - EditionTkaTableStubComponent, - CompileHtmlComponent, - RouterLinkStubDirective - ] + declarations: [TextcriticsComponent, CriticsListStubComponent] }).compileComponents(); })); diff --git a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts index 58a578a91d..8ae11d3c1a 100644 --- a/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts +++ b/src/app/views/edition-view/edition-outlets/report/textcritics/textcritics.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { TextcriticalComment, TextcriticsList } from '@awg-views/edition-view/models'; +import { TextcriticsList } from '@awg-views/edition-view/models'; /** * The TextcriticalComment component. @@ -41,20 +41,6 @@ export class TextcriticsComponent implements OnInit { @Output() selectSvgSheetRequest: EventEmitter = new EventEmitter(); - /** - * Self-referring variable needed for CompileHtml library. - */ - ref: TextcriticsComponent; - - /** - * Constructor of the TextcriticsComponent. - * - * It initializes the self-referring ref variable needed for CompileHtml library. - */ - constructor() { - this.ref = this; - } - /** * Angular life cycle hook: ngOnInit. * @@ -88,17 +74,4 @@ export class TextcriticsComponent implements OnInit { selectSvgSheet(id: string): void { this.selectSvgSheetRequest.emit(id); } - - /** - * Public method: isNotEmptyArray. - * - * It checks if a given array of the textcritical comment input - * is not empty. - * - * @param {TextcriticalComment[] | string[]} part The given array input. - * @returns {boolean} The boolean result of the check. - */ - isNotEmptyArray(checkArray: TextcriticalComment[] | string[]): boolean { - return checkArray && checkArray.constructor === Array && checkArray.length > 0; - } } From ecbb32b228438a19768ba8b83ef85ad07d7c90d0 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sun, 9 Feb 2020 02:45:44 +0100 Subject: [PATCH 18/30] fix(edition): handle placeholder for op. 12 Aa:SkI/1 --- .../edition-detail.component.ts | 78 +++++++++---------- .../edition-svg-sheet.component.html | 8 ++ .../edition-svg-sheet.component.ts | 2 + .../series1/section5/op12/svg-sheets.json | 6 ++ 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts index 770ae91d0d..afaba9e129 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts @@ -164,45 +164,6 @@ export class EditionDetailComponent implements OnInit { ); } - /** - * Private method: getSketchParams. - * - * It checks the route params for a sketch query - * and returns the id of the selected sheet. - * - * @default first entry of this.svgSheetsData - * - * @param {ParamMap} queryParams The query paramMap of the activated route. - * @returns {string} The id of the selected sheet. - */ - private getSketchParams(queryParams?: ParamMap): string { - // if there is no id in query params - // take first entry of svg sheets data as default - if (!queryParams.get('sketch')) { - this.onSvgSheetSelect(this.svgSheetsData.sheets[0].id); - return; - } - return queryParams.get('sketch') ? queryParams.get('sketch') : this.svgSheetsData.sheets[0].id; - } - - /** - * Private method: setSelectedSvgSheet. - * - * It sets the selectedSvg from a given id. - * - * @param {string} id The given id input. - * @returns {EditionSvgSheet} The selected sheet. - */ - private setSelectedSvgSheet(id: string): EditionSvgSheet { - if (!id) { - return; - } - // find index of given id in svgSheetsData.sheets array - const sheetIndex = this.svgSheetsData.sheets.findIndex(sheets => sheets.id === id); - // return the sheet with the given id - return this.svgSheetsData.sheets[sheetIndex]; - } - /** * Public method: onOverlaySelect. * @@ -255,4 +216,43 @@ export class EditionDetailComponent implements OnInit { this.router.navigate([this.editionWork.baseRoute, this.editionWork.detailRoute], navigationExtras); } + + /** + * Private method: getSketchParams. + * + * It checks the route params for a sketch query + * and returns the id of the selected sheet. + * + * @default first entry of this.svgSheetsData + * + * @param {ParamMap} queryParams The query paramMap of the activated route. + * @returns {string} The id of the selected sheet. + */ + private getSketchParams(queryParams?: ParamMap): string { + // if there is no id in query params + // take first entry of svg sheets data as default + if (!queryParams.get('sketch')) { + this.onSvgSheetSelect(this.svgSheetsData.sheets[0].id); + return; + } + return queryParams.get('sketch') ? queryParams.get('sketch') : this.svgSheetsData.sheets[0].id; + } + + /** + * Private method: setSelectedSvgSheet. + * + * It sets the selectedSvg from a given id. + * + * @param {string} id The given id input. + * @returns {EditionSvgSheet} The selected sheet. + */ + private setSelectedSvgSheet(id: string): EditionSvgSheet { + if (!id) { + return; + } + // find index of given id in svgSheetsData.sheets array + const sheetIndex = this.svgSheetsData.sheets.findIndex(sheets => sheets.id === id); + // return the sheet with the given id + return this.svgSheetsData.sheets[sheetIndex]; + } } diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-svg-sheet/edition-svg-sheet.component.html b/src/app/views/edition-view/edition-outlets/edition-detail/edition-svg-sheet/edition-svg-sheet.component.html index 9fe95576ed..ed4d925d48 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-svg-sheet/edition-svg-sheet.component.html +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-svg-sheet/edition-svg-sheet.component.html @@ -1,4 +1,12 @@
      + +
      +

      + [Der edierte Notentext von Aa:SKI/1 erscheint im Zusammenhang der vollständigen Edition der + Vier Lieder op. 12 in AWG I/5.] +

      +
      +
      Date: Sun, 9 Feb 2020 21:04:45 +0100 Subject: [PATCH 19/30] fix(edition): get selectability of convolute item from data --- .../edition-folio/folio.service.ts | 4 +- .../models/folio-calculation.model.ts | 24 ++++- .../models/folio-svg-data.model.ts | 13 +++ .../views/edition-view/models/folio.model.ts | 10 ++ .../section5/op12/folio-convolute.json | 10 ++ .../section5/op25/folio-convolute.json | 95 ++++++++++++++++++- 6 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts b/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts index 346ae06169..8f6cfe8ed0 100644 --- a/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts +++ b/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts @@ -331,8 +331,8 @@ export class FolioService { // add click event handler // exclude and mute sketch Aa:SkI for now - if (contentItem.sigle === 'Aa:SkI/1a' || contentItem.sigle === 'Aa:SkI/1b') { - snapItemGroup.click(() => this.ref.openModal('op12_sourceNotA')); + if (contentItem.selectable === false) { + snapItemGroup.click(() => this.ref.openModal(contentItem.linkTo)); snapItemGroup.attr({ stroke: 'grey', fill: 'grey' diff --git a/src/app/views/edition-view/models/folio-calculation.model.ts b/src/app/views/edition-view/models/folio-calculation.model.ts index e3db3b0d0a..a6df6e9364 100644 --- a/src/app/views/edition-view/models/folio-calculation.model.ts +++ b/src/app/views/edition-view/models/folio-calculation.model.ts @@ -227,14 +227,24 @@ export class FolioCalculationContentItem { previous: FolioCalculationContentItemCache; /** - * The optional label for the sigle of the content item (string). + * The label for the sigle of the content item (string). */ - sigle?: string; + sigle: string; /** - * The optional label for the measure of the content item (string). + * The label for the measure of the content item (string). */ - measure?: string; + measure: string; + + /** + * The boolean flag if the content item can be selected.. + */ + selectable: boolean; + + /** + * The link to a convolute description in the critical report. + */ + linkTo: string; } /** @@ -582,6 +592,12 @@ export class FolioCalculation { calculatedContentItem.sigle = content.sigle; calculatedContentItem.measure = content.measure; + calculatedContentItem.selectable = true; + calculatedContentItem.linkTo = ''; + if (content['selectable'] === false && content['linkTo']) { + calculatedContentItem.selectable = content.selectable; + calculatedContentItem.linkTo = content.linkTo; + } calculatedContentItems.push(calculatedContentItem); }); diff --git a/src/app/views/edition-view/models/folio-svg-data.model.ts b/src/app/views/edition-view/models/folio-svg-data.model.ts index bf0d9008b3..60e6b93334 100644 --- a/src/app/views/edition-view/models/folio-svg-data.model.ts +++ b/src/app/views/edition-view/models/folio-svg-data.model.ts @@ -108,6 +108,16 @@ class FolioSvgContentItem { */ measure: string; + /** + * The optional boolean flag if the content item can be selected.. + */ + selectable: boolean; + + /** + * The optional link to a convolute description in the critical report. + */ + linkTo: string; + /** * The upper left corner of a content item (FolioCalculationPoint). * @@ -144,6 +154,9 @@ class FolioSvgContentItem { constructor(calculatedContentItem: FolioCalculationContentItem) { this.sigle = calculatedContentItem.sigle; this.measure = calculatedContentItem.measure; + this.selectable = calculatedContentItem.selectable; + this.linkTo = calculatedContentItem.linkTo; + this.upperLeftCorner = calculatedContentItem.current.cornerPoints.upperLeftCorner; this.width = calculatedContentItem.width; this.height = calculatedContentItem.height; diff --git a/src/app/views/edition-view/models/folio.model.ts b/src/app/views/edition-view/models/folio.model.ts index d55b754714..4ecacedd65 100644 --- a/src/app/views/edition-view/models/folio.model.ts +++ b/src/app/views/edition-view/models/folio.model.ts @@ -102,6 +102,16 @@ export class FolioContent { */ measure: string; + /** + * Boolean flag if the content item can be selected.. + */ + selectable?: boolean; + + /** + * The link to a convolute description in the critical report. + */ + linkTo?: string; + /** * The folio content's optional sectionPartition (number). */ diff --git a/src/assets/data/edition/series1/section5/op12/folio-convolute.json b/src/assets/data/edition/series1/section5/op12/folio-convolute.json index f71baf2fbe..ce37cb9214 100644 --- a/src/assets/data/edition/series1/section5/op12/folio-convolute.json +++ b/src/assets/data/edition/series1/section5/op12/folio-convolute.json @@ -14,6 +14,8 @@ { "sigle": "Aa:SkI/1a", "measure": "1–2, [3–6]", + "selectable": false, + "linkTo": "op12_sourceNotA", "sectionPartition": 1, "sections": [ { @@ -24,6 +26,8 @@ }, { "sigle": "Aa:SkI/1a", + "selectable": false, + "linkTo": "op12_sourceNotA", "measure": "[7–12]", "sectionPartition": 1, "sections": [ @@ -36,6 +40,8 @@ }, { "sigle": "Aa:SkI/1a", + "selectable": false, + "linkTo": "op12_sourceNotA", "measure": "[13–17]", "sectionPartition": 1, "sections": [ @@ -116,6 +122,8 @@ "content": [ { "sigle": "Aa:SkI/1b", + "selectable": false, + "linkTo": "op12_sourceNotA", "measure": "[18–22]", "sections": [ { @@ -126,6 +134,8 @@ }, { "sigle": "Aa:SkI/1b", + "selectable": false, + "linkTo": "op12_sourceNotA", "measure": "[23–24]", "sectionPartition": 1, "sections": [ diff --git a/src/assets/data/edition/series1/section5/op25/folio-convolute.json b/src/assets/data/edition/series1/section5/op25/folio-convolute.json index b8955f705a..221344ee8a 100644 --- a/src/assets/data/edition/series1/section5/op25/folio-convolute.json +++ b/src/assets/data/edition/series1/section5/op25/folio-convolute.json @@ -10,7 +10,100 @@ "height": 270, "width": 335 }, - "content": [] + "content": [ + { + "sigle": "A:SkI/2", + "measure": "", + "sectionPartition": 4, + "sections": [ + { + "position": 1, + "startSystem": 1, + "endSystem": 1 + } + ] + }, + { + "sigle": "A:SkI/1", + "measure": "1–2", + "sectionPartition": 4, + "sections": [ + { + "position": 2, + "startSystem": 1, + "endSystem": 1 + } + ] + }, + { + "sigle": "A:SkI/2.1", + "measure": "1–4", + "selectable": false, + "linkTo": "op25_sourceNotA", + "sectionPartition": 4, + "sections": [ + { + "position": 3, + "startSystem": 1, + "endSystem": 3 + } + ] + }, + { + "sigle": "A:SkI/2.1.1", + "measure": "1–2", + "selectable": false, + "linkTo": "op25_sourceNotA", + "sectionPartition": 4, + "sections": [ + { + "position": 4, + "startSystem": 1, + "endSystem": 1 + } + ] + }, + { + "sigle": "A:SkI/2.1.2", + "measure": "3", + "selectable": false, + "linkTo": "op25_sourceNotA", + "sectionPartition": 4, + "sections": [ + { + "position": 1, + "startSystem": 2, + "endSystem": 2 + } + ] + }, + { + "sigle": "A:SkI/2.1.2.1", + "measure": "3", + "selectable": false, + "linkTo": "op25_sourceNotA", + "sectionPartition": 4, + "sections": [ + { + "position": 3, + "startSystem": 4, + "endSystem": 4 + } + ] + }, + { + "sigle": "A:SkI/3", + "measure": "1–5", + "sectionPartition": 4, + "sections": [ + { + "position": 1, + "startSystem": 3, + "endSystem": 3 + } + ] + } + ] }, { "folioId": "39r", From 8c871cc7f435910daea3789a0c2b111982904a29 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sun, 9 Feb 2020 23:41:37 +0100 Subject: [PATCH 20/30] fix(edition): improve folio handling and rendering --- ...onent.css => folio-overview.component.css} | 0 ...ent.html => folio-overview.component.html} | 0 ...ec.ts => folio-overview.component.spec.ts} | 10 +-- ...mponent.ts => folio-overview.component.ts} | 38 +++++------ .../edition-folio/folio.module.ts | 8 +-- .../edition-folio/folio.service.ts | 26 ++++---- .../section5/op25/folio-convolute.json | 64 +++++++++++++------ 7 files changed, 84 insertions(+), 62 deletions(-) rename src/app/views/edition-view/edition-outlets/edition-folio/{folio.component.css => folio-overview.component.css} (100%) rename src/app/views/edition-view/edition-outlets/edition-folio/{folio.component.html => folio-overview.component.html} (100%) rename src/app/views/edition-view/edition-outlets/edition-folio/{folio.component.spec.ts => folio-overview.component.spec.ts} (91%) rename src/app/views/edition-view/edition-outlets/edition-folio/{folio.component.ts => folio-overview.component.ts} (92%) diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.css b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.css similarity index 100% rename from src/app/views/edition-view/edition-outlets/edition-folio/folio.component.css rename to src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.css diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.html b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.html similarity index 100% rename from src/app/views/edition-view/edition-outlets/edition-folio/folio.component.html rename to src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.html diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.spec.ts b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.spec.ts similarity index 91% rename from src/app/views/edition-view/edition-outlets/edition-folio/folio.component.spec.ts rename to src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.spec.ts index 9c4f28b584..ec10b46cd1 100644 --- a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.spec.ts +++ b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.spec.ts @@ -1,24 +1,24 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FolioComponent } from './folio.component'; +import { FolioOverviewComponent } from './folio-overview.component'; import { FolioService } from './folio.service'; import { Folio, FolioSvgData, EditionSvgSheet, FolioCalculation, FolioSettings } from '@awg-views/edition-view/models'; describe('FolioComponent', () => { - let component: FolioComponent; - let fixture: ComponentFixture; + let component: FolioOverviewComponent; + let fixture: ComponentFixture; let expectedFolios: Folio[]; let expectedSvgSheet: EditionSvgSheet; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [FolioComponent], + declarations: [FolioOverviewComponent], providers: [FolioService] }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(FolioComponent); + fixture = TestBed.createComponent(FolioOverviewComponent); component = fixture.componentInstance; }); diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.ts b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.ts similarity index 92% rename from src/app/views/edition-view/edition-outlets/edition-folio/folio.component.ts rename to src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.ts index 6dc0b3c37a..86f29d1c4c 100644 --- a/src/app/views/edition-view/edition-outlets/edition-folio/folio.component.ts +++ b/src/app/views/edition-view/edition-outlets/edition-folio/folio-overview.component.ts @@ -1,12 +1,12 @@ import { AfterViewChecked, - AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, - OnInit, - Output + OnChanges, + Output, + SimpleChanges } from '@angular/core'; import { Folio, FolioSettings, FolioSvgData, EditionSvgSheet, ViewBox } from '@awg-views/edition-view/models'; @@ -28,11 +28,11 @@ declare var Snap: any; */ @Component({ selector: 'awg-edition-folio', - templateUrl: './folio.component.html', - styleUrls: ['./folio.component.css'], + templateUrl: './folio-overview.component.html', + styleUrls: ['./folio-overview.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class FolioComponent implements OnInit, AfterViewInit, AfterViewChecked { +export class FolioOverviewComponent implements OnChanges, AfterViewChecked { /** * Input variable: folios. * @@ -134,7 +134,7 @@ export class FolioComponent implements OnInit, AfterViewInit, AfterViewChecked { /** * Self-referring variable needed for CompileHtml library. */ - ref: FolioComponent; + ref: FolioOverviewComponent; /** * Constructor of the FolioComponent. @@ -150,24 +150,14 @@ export class FolioComponent implements OnInit, AfterViewInit, AfterViewChecked { } /** - * Angular life cycle hook: ngOnInit. + * Angular life cycle hook: ngOnChanges. * - * It calls the containing methods - * when initializing the component. - */ - ngOnInit() { - this.prepareFolioSvgOutput(); - } - - /** - * Angular life cycle hook: ngAfterViewInit. + * It checks for changes of the given input. * - * It calls the containing methods - * after the view was initialized. + * @param {SimpleChanges} changes The changes of the input. */ - ngAfterViewInit() { - // start to render svg only after view, inputs and calculation are available - this.renderSnapSvg(); + ngOnChanges(changes: SimpleChanges) { + this.prepareFolioSvgOutput(); } /** @@ -177,6 +167,8 @@ export class FolioComponent implements OnInit, AfterViewInit, AfterViewChecked { * after the view was built and checked. */ ngAfterViewChecked() { + // start to render svg only after view, inputs and calculation are available + this.renderSnapSvg(); // toggle active classes after view was checked this.toggleActiveClass(); } @@ -248,7 +240,7 @@ export class FolioComponent implements OnInit, AfterViewInit, AfterViewChecked { this.canvasArray = []; /* apply data from folioSvgDataArray to render the svg image with snapsvg */ - this.folioSvgDataArray.forEach((folioSvg: FolioSvgData, folioIndex: number) => { + this.folioSvgDataArray.map((folioSvg: FolioSvgData, folioIndex: number) => { // init canvas const snapId: string = '#folio-' + folioSvg.sheet.folioId; const snapCanvas: any = Snap(snapId); diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.module.ts b/src/app/views/edition-view/edition-outlets/edition-folio/folio.module.ts index dbb9f255c0..9c07bf4953 100644 --- a/src/app/views/edition-view/edition-outlets/edition-folio/folio.module.ts +++ b/src/app/views/edition-view/edition-outlets/edition-folio/folio.module.ts @@ -1,17 +1,17 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '@awg-shared/shared.module'; -import { FolioComponent } from './folio.component'; +import { FolioOverviewComponent } from './folio-overview.component'; /** * The edition folio module. * - * It embeds the {@link FolioComponent} + * It embeds the {@link FolioOverviewComponent} * as well as the {@link SharedModule}. */ @NgModule({ imports: [SharedModule], - declarations: [FolioComponent], - exports: [FolioComponent] + declarations: [FolioOverviewComponent], + exports: [FolioOverviewComponent] }) export class FolioModule {} diff --git a/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts b/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts index 8f6cfe8ed0..a876e3cf65 100644 --- a/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts +++ b/src/app/views/edition-view/edition-outlets/edition-folio/folio.service.ts @@ -267,27 +267,31 @@ export class FolioService { // init const centeredXPosition = contentItem.upperLeftCorner.x + contentItem.width / 2; const centeredYPosition = contentItem.upperLeftCorner.y + contentItem.height / 2; - const itemLabelArray: string[] = [contentItem.sigle, ' T. ' + contentItem.measure]; + const itemLabelArray: string[] = contentItem.measure + ? [contentItem.sigle, ' T. ' + contentItem.measure] + : [contentItem.sigle]; const snapItemLabel: any = snapCanvas.text(0, 0, itemLabelArray); snapItemLabel.attr({ class: 'item-label', - fontSize: '18px' + style: 'font: 12px Source Sans Pro, source-sans-pro, sans-serif', + dominantBaseline: 'middle' }); // attributes for tspan elements of itemLabel array + const textAnchor = 'middle'; snapItemLabel.select('tspan:first-of-type').attr({ x: centeredXPosition, y: centeredYPosition, - alignmentBaseline: 'middle', - textAnchor: 'middle' - }); - snapItemLabel.select('tspan:last-of-type').attr({ - x: centeredXPosition, - y: centeredYPosition, - alignmentBaseline: 'middle', - textAnchor: 'middle', - dy: '1.2em' + textAnchor }); + if (itemLabelArray.length > 1) { + snapItemLabel.select('tspan:last-of-type').attr({ + x: centeredXPosition, + y: centeredYPosition, + textAnchor, + dy: '1.2em' + }); + } // item shape const snapItemShape = snapCanvas.group(); diff --git a/src/assets/data/edition/series1/section5/op25/folio-convolute.json b/src/assets/data/edition/series1/section5/op25/folio-convolute.json index 221344ee8a..9bb6cb9168 100644 --- a/src/assets/data/edition/series1/section5/op25/folio-convolute.json +++ b/src/assets/data/edition/series1/section5/op25/folio-convolute.json @@ -14,7 +14,7 @@ { "sigle": "A:SkI/2", "measure": "", - "sectionPartition": 4, + "sectionPartition": 5, "sections": [ { "position": 1, @@ -25,8 +25,8 @@ }, { "sigle": "A:SkI/1", - "measure": "1–2", - "sectionPartition": 4, + "measure": "", + "sectionPartition": 5, "sections": [ { "position": 2, @@ -37,13 +37,13 @@ }, { "sigle": "A:SkI/2.1", - "measure": "1–4", + "measure": "", "selectable": false, "linkTo": "op25_sourceNotA", - "sectionPartition": 4, + "sectionPartition": 5, "sections": [ { - "position": 3, + "position": 4, "startSystem": 1, "endSystem": 3 } @@ -51,13 +51,13 @@ }, { "sigle": "A:SkI/2.1.1", - "measure": "1–2", + "measure": "", "selectable": false, "linkTo": "op25_sourceNotA", - "sectionPartition": 4, + "sectionPartition": 5, "sections": [ { - "position": 4, + "position": 5, "startSystem": 1, "endSystem": 1 } @@ -65,13 +65,13 @@ }, { "sigle": "A:SkI/2.1.2", - "measure": "3", + "measure": "", "selectable": false, "linkTo": "op25_sourceNotA", - "sectionPartition": 4, + "sectionPartition": 5, "sections": [ { - "position": 1, + "position": 3, "startSystem": 2, "endSystem": 2 } @@ -79,13 +79,13 @@ }, { "sigle": "A:SkI/2.1.2.1", - "measure": "3", + "measure": "", "selectable": false, "linkTo": "op25_sourceNotA", - "sectionPartition": 4, + "sectionPartition": 8, "sections": [ { - "position": 3, + "position": 5, "startSystem": 4, "endSystem": 4 } @@ -93,8 +93,8 @@ }, { "sigle": "A:SkI/3", - "measure": "1–5", - "sectionPartition": 4, + "measure": "", + "sectionPartition": 2, "sections": [ { "position": 1, @@ -102,6 +102,32 @@ "endSystem": 3 } ] + }, + { + "sigle": "A:SkI/4a", + "measure": "{1A}–3A", + "sectionPartition": 1, + "sections": [ + { + "position": 1, + "startSystem": 9, + "endSystem": 12 + } + ] + }, + { + "sigle": "A:SkI/4.1", + "measure": "", + "selectable": false, + "linkTo": "op25_sourceNotA", + "sectionPartition": 3, + "sections": [ + { + "position": 3, + "startSystem": 13, + "endSystem": 15 + } + ] } ] }, @@ -121,7 +147,7 @@ "folios": [ { "folioId": "38v", - "systems": "16", + "systems": "18", "format": { "height": 270, "width": 335 @@ -130,7 +156,7 @@ }, { "folioId": "39r", - "systems": "16", + "systems": "18", "format": { "height": 270, "width": 335 From fb77f72aa9c817c8e0d09e2e9d204dfbf8508683 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Sun, 9 Feb 2020 23:45:49 +0100 Subject: [PATCH 21/30] fix(edition): move convolute logic to parent component (edition detail) --- .../edition-convolute.component.ts | 40 ++++--- .../edition-detail.component.html | 2 + .../edition-detail.component.ts | 107 ++++++++++++++---- 3 files changed, 112 insertions(+), 37 deletions(-) diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-convolute/edition-convolute.component.ts b/src/app/views/edition-view/edition-outlets/edition-detail/edition-convolute/edition-convolute.component.ts index 65f49cec86..20a84da8d3 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-convolute/edition-convolute.component.ts +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-convolute/edition-convolute.component.ts @@ -43,6 +43,14 @@ export class EditionConvoluteComponent implements OnInit { @Input() folioConvoluteData: FolioConvoluteList; + /** + * Public variable: selectedConvolute. + * + * It keeps the selected convolute. + */ + @Input() + selectedConvolute: FolioConvolute; + /** * Public variable: selectedSvgSheet. * @@ -60,6 +68,14 @@ export class EditionConvoluteComponent implements OnInit { @Output() openModalRequest: EventEmitter = new EventEmitter(); + /** + * Output variable: selectConvoluteRequest. + * + * It keeps an event emitter for the selected convolute. + */ + @Output() + selectConvoluteRequest: EventEmitter = new EventEmitter(); + /** * Output variable: selectSvgSheetRequest. * @@ -75,13 +91,6 @@ export class EditionConvoluteComponent implements OnInit { */ faSquare = faSquare; - /** - * Public variable: selectedConvolute. - * - * It keeps the selected convolute. - */ - selectedConvolute: FolioConvolute; - /** * Public variable: folioLegends. * @@ -128,6 +137,9 @@ export class EditionConvoluteComponent implements OnInit { * @returns {void} Emits the id. */ openModal(id: string): void { + if (!id) { + return; + } this.openModalRequest.emit(id); } @@ -144,16 +156,7 @@ export class EditionConvoluteComponent implements OnInit { if (!id) { return; } - const convoluteIndex = this.folioConvoluteData.convolutes.findIndex(c => c.convoluteId === id); - const convolute: FolioConvolute = this.folioConvoluteData.convolutes[convoluteIndex]; - if (convolute.folios && convolute.folios.constructor === Array && convolute.folios.length === 0) { - // if no folio data provided, open modal - if (convolute.linkTo) { - this.openModal(convolute.linkTo); - } - return; - } - this.selectedConvolute = convolute; + this.selectConvoluteRequest.emit(id); } /** @@ -166,6 +169,9 @@ export class EditionConvoluteComponent implements OnInit { * @returns {void} Emits the id. */ selectSvgSheet(id: string): void { + if (!id) { + return; + } this.selectSvgSheetRequest.emit(id); } } diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.html b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.html index c7b6d2f051..4e87c6a482 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.html +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.html @@ -9,8 +9,10 @@ diff --git a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts index afaba9e129..414a69c7d1 100644 --- a/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts +++ b/src/app/views/edition-view/edition-outlets/edition-detail/edition-detail.component.ts @@ -1,13 +1,15 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, NavigationExtras, ParamMap, Router } from '@angular/router'; import { switchMap } from 'rxjs/operators'; +import { ModalComponent } from '@awg-shared/modal/modal.component'; import { EditionSvgOverlay, EditionSvgSheet, EditionSvgSheetList, EditionWork, + FolioConvolute, FolioConvoluteList, TextcriticalComment, TextcriticsList @@ -29,6 +31,8 @@ import { EditionDataService, EditionService } from '@awg-views/edition-view/serv styleUrls: ['./edition-detail.component.css'] }) export class EditionDetailComponent implements OnInit { + @ViewChild('modal', { static: true }) modal: ModalComponent; + /** * Public variable: editionWork. * @@ -57,6 +61,20 @@ export class EditionDetailComponent implements OnInit { */ textcriticsData: TextcriticsList; + /** + * Public variable: selectedConvolute. + * + * It keeps the selected convolute. + */ + selectedConvolute: FolioConvolute; + + /** + * Public variable: selectedOverlay. + * + * It keeps the selected svg overlay. + */ + selectedOverlay: EditionSvgOverlay; + /** * Public variable: selectedSvgSheet. * @@ -71,13 +89,6 @@ export class EditionDetailComponent implements OnInit { */ selectedTextcriticalComments: TextcriticalComment[]; - /** - * Public variable: selectedOverlay. - * - * It keeps the selected svg overlay. - */ - selectedOverlay: EditionSvgOverlay; - /** * Public variable: errorMessage. * @@ -156,7 +167,7 @@ export class EditionDetailComponent implements OnInit { .subscribe( (queryParams: ParamMap) => { const sheetId: string = this.getSketchParams(queryParams); - this.selectedSvgSheet = this.setSelectedSvgSheet(sheetId); + this.selectedSvgSheet = this.findSvgSheet(sheetId); }, error => { this.errorMessage = error as any; @@ -164,6 +175,30 @@ export class EditionDetailComponent implements OnInit { ); } + /** + * Public method: onConvoluteSelect. + * + * It selects a convolute by its id. + * + * @param {string} id The given id. + * @returns {void} Sets the selectedConvolute variable. + */ + onConvoluteSelect(id: string): void { + if (!id) { + return; + } + const convolute: FolioConvolute = this.findConvolute(id); + + if (convolute.folios && convolute.folios.constructor === Array && convolute.folios.length === 0) { + // if no folio data provided, open modal + if (convolute.linkTo) { + this.modal.open(convolute.linkTo); + } + return; + } + this.selectedConvolute = convolute; + } + /** * Public method: onOverlaySelect. * @@ -177,12 +212,7 @@ export class EditionDetailComponent implements OnInit { if (!this.textcriticsData && !this.selectedSvgSheet) { return; } - - // shortcut - const textcriticsIndex = this.textcriticsData.textcritics.findIndex(textcritic => { - return textcritic.id === this.selectedSvgSheet.id; - }); - const textcriticalComments = this.textcriticsData.textcritics[textcriticsIndex].comments; + const textcriticalComments: TextcriticalComment[] = this.findTextCriticalComments(); this.selectedOverlay = overlay; this.selectedTextcriticalComments = this.editionService.getTextcriticalComments( @@ -206,7 +236,7 @@ export class EditionDetailComponent implements OnInit { if (!id) { id = ''; } - this.selectedSvgSheet = this.setSelectedSvgSheet(id); + this.selectedSvgSheet = this.findSvgSheet(id); this.showTkA = false; const navigationExtras: NavigationExtras = { @@ -239,14 +269,32 @@ export class EditionDetailComponent implements OnInit { } /** - * Private method: setSelectedSvgSheet. + * Private method: findConvolute. + * + * It finds a convolute with a given id. + * + * @param {string} id The given id input. + * @returns {FolioConvolute} The convolute that was found. + */ + private findConvolute(id: string): FolioConvolute { + if (!id) { + return; + } + // find index of given id in folioConvoluteData.convolutes array + const convoluteIndex = this.folioConvoluteData.convolutes.findIndex(convolute => convolute.convoluteId === id); + // return the convolute with the given id + return this.folioConvoluteData.convolutes[convoluteIndex]; + } + + /** + * Private method: findSvgSheet. * - * It sets the selectedSvg from a given id. + * It finds a svg sheet with a given id. * * @param {string} id The given id input. - * @returns {EditionSvgSheet} The selected sheet. + * @returns {EditionSvgSheet} The sheet that was found. */ - private setSelectedSvgSheet(id: string): EditionSvgSheet { + private findSvgSheet(id: string): EditionSvgSheet { if (!id) { return; } @@ -255,4 +303,23 @@ export class EditionDetailComponent implements OnInit { // return the sheet with the given id return this.svgSheetsData.sheets[sheetIndex]; } + + /** + * Private method: findTextCriticalComments. + * + * It finds the textcritical comments for an svg overlay. + * + * @returns {TextcriticalComment[]} The textcritical comments that were found. + */ + private findTextCriticalComments(): TextcriticalComment[] { + if (!this.textcriticsData && !this.selectedSvgSheet) { + return; + } + // find index of teh selected svg sheet id in textcriticsData.textcritics array + const textcriticsIndex = this.textcriticsData.textcritics.findIndex( + textcritic => textcritic.id === this.selectedSvgSheet.id + ); + // return the comments with the given id + return this.textcriticsData.textcritics[textcriticsIndex].comments; + } } From 1f25f63148e61f00461496d25081a448a6310091 Mon Sep 17 00:00:00 2001 From: "Stefan@Lap" Date: Mon, 10 Feb 2020 01:16:29 +0100 Subject: [PATCH 22/30] feat(edition): prepare embedding of op25 sheets --- .../edition-accolade.component.html | 7 +- .../edition-svg-sheet.component.html | 198 +++++++++++++++--- .../edition-svg-sheet.component.ts | 87 +++++++- .../section5/op25/folio-convolute.json | 67 +++++- 4 files changed, 317 insertions(+), 42 deletions(-) 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 @@
      TaktSystemTaktSystem Ort im Takt Kommentar
      {{ textcriticalComment.measure }}{{ textcriticalComment.system }}