From 584f6b93bf073e3c8ac2156ac0961e8f1f356041 Mon Sep 17 00:00:00 2001 From: Mathis Hofer Date: Mon, 29 Apr 2019 14:44:48 +0200 Subject: [PATCH] Add settings.js and SettingsService #40 --- .gitignore | 3 + README.md | 23 ++++++- angular.json | 9 ++- src/app/shared/settings.service.spec.ts | 89 +++++++++++++++++++++++++ src/app/shared/settings.service.ts | 52 +++++++++++++++ src/index.html | 1 + src/settings.example.js | 8 +++ 7 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/settings.service.spec.ts create mode 100644 src/app/shared/settings.service.ts create mode 100644 src/settings.example.js diff --git a/.gitignore b/.gitignore index f4f46a5fe..4c4700de0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ testem.log # System Files .DS_Store Thumbs.db + +# Project-specific +/src/settings.js diff --git a/README.md b/README.md index 277ed3cc4..fa216e9e1 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,30 @@ JavaScript Web Modul zu Abbildung des «Absenzenverwaltung» Prozesses mit dem C TODO: - Latest build -- Configuration - Prerequisites (localStorage values etc.), usage of `index.html` +Um die Absenzverwaltung auf einer Webseite zu integrieren, muss aus der `index.html`-Datei folgendes übernommen werden: + +``` + + + + +``` + +Und (alle ` + ... + + +``` + +Weiter muss die Datei `settings.example.js` nach `settings.js` umbenannt werden und deren Inhalt angepasst werden. + ## Entwicklung - Allgemeine Aspekte sind im [Wiki](https://github.com/erz-mba-fbi/absenzenmanagement/wiki) dokumentiert diff --git a/angular.json b/angular.json index e36812407..119ccbf79 100644 --- a/angular.json +++ b/angular.json @@ -18,7 +18,14 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json", - "assets": ["src/favicon.ico", "src/assets"], + "assets": [ + "src/favicon.ico", + "src/settings.js", + "src/settings.example.js", + "src/assets", + { "glob": "README.md", "input": ".", "output": "." }, + { "glob": "**/*.md", "input": "doc", "output": "doc" } + ], "styles": ["src/styles.css"], "scripts": [], "es5BrowserSupport": true diff --git a/src/app/shared/settings.service.spec.ts b/src/app/shared/settings.service.spec.ts new file mode 100644 index 000000000..8cd8a6c1e --- /dev/null +++ b/src/app/shared/settings.service.spec.ts @@ -0,0 +1,89 @@ +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { SettingsService, Settings } from './settings.service'; + +describe('SettingsService', () => { + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + describe('.settings$', () => { + let settingsMock: Settings; + let next: jasmine.Spy; + let error: jasmine.Spy; + let complete: jasmine.Spy; + beforeEach(() => { + settingsMock = { apiUrl: 'https://example.com/api' }; + + next = jasmine.createSpy('next'); + error = jasmine.createSpy('error'); + complete = jasmine.createSpy('complete'); + }); + + afterEach(() => resetSettings()); + + it('emits settings object', () => { + setSettings(settingsMock); + const service: SettingsService = TestBed.get(SettingsService); + + service.settings$.subscribe(next, error, complete); + expect(next).toHaveBeenCalledWith(settingsMock); + expect(error).not.toHaveBeenCalled(); + expect(complete).toHaveBeenCalled(); + }); + + it('emits settings object, when available after 2s', fakeAsync(() => { + const service: SettingsService = TestBed.get(SettingsService); + service.settings$.subscribe(next, error, complete); + expect(next).not.toHaveBeenCalled(); + + tick(1000); + expect(next).not.toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + expect(complete).not.toHaveBeenCalled(); + + tick(1000); + expect(next).not.toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + expect(complete).not.toHaveBeenCalled(); + + setSettings(settingsMock); + tick(1000); + expect(next).toHaveBeenCalledWith(settingsMock); + expect(error).not.toHaveBeenCalled(); + expect(complete).toHaveBeenCalled(); + })); + + it('throws error when settings loading failed or not defined', fakeAsync(() => { + const service: SettingsService = TestBed.get(SettingsService); + service.settings$.subscribe(next, error, complete); + expect(next).not.toHaveBeenCalled(); + + tick(6000); + expect(next).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalled(); + expect(error.calls.mostRecent().args[0].toString()).toEqual( + 'Error: Settings not available' + ); + expect(complete).not.toHaveBeenCalled(); + })); + }); + + describe('apiUrl$', () => { + it('returns apiUrl', () => { + setSettings({ apiUrl: 'https://example.com/api' }); + const service: SettingsService = TestBed.get(SettingsService); + const callback = jasmine.createSpy('callback'); + service.apiUrl$.subscribe(callback); + expect(callback).toHaveBeenCalledWith('https://example.com/api'); + }); + }); + + function setSettings(settings: Settings): void { + (window as any).absenzenmanagement = { settings }; + } + + function resetSettings(): void { + (window as any).absenzenmanagement = undefined; + } +}); diff --git a/src/app/shared/settings.service.ts b/src/app/shared/settings.service.ts new file mode 100644 index 000000000..733a77bbb --- /dev/null +++ b/src/app/shared/settings.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { defer, of, Observable } from 'rxjs'; +import { map, retryWhen, delay, pluck, shareReplay } from 'rxjs/operators'; + +export interface Settings { + apiUrl: string; +} + +const SETTINGS_RETRY_COUNT = 10; +const SETTINGS_RETRY_DELAY = 500; + +@Injectable({ + providedIn: 'root' +}) +export class SettingsService { + settings$ = this.loadSettings(); + apiUrl$ = this.settings$.pipe(pluck('apiUrl')); + + private loadSettings(): Observable { + return defer(() => of(this.settings)).pipe( + map(settings => { + if (settings == null) { + throw new Error('Settings not available'); + } + return settings; + }), + retryWhen(this.retryNotifier$), + shareReplay(1) + ); + } + + private retryNotifier$(errors$: Observable): Observable { + let retries = 0; + return errors$.pipe( + delay(SETTINGS_RETRY_DELAY), + map(error => { + if (retries++ === SETTINGS_RETRY_COUNT) { + throw error; + } + return error; + }) + ); + } + + private get settings(): Option { + return ( + ((window as any).absenzenmanagement && + (window as any).absenzenmanagement.settings) || + null + ); + } +} diff --git a/src/index.html b/src/index.html index 6b69dd202..7a2df9cb1 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,7 @@ + diff --git a/src/settings.example.js b/src/settings.example.js new file mode 100644 index 000000000..72748a55a --- /dev/null +++ b/src/settings.example.js @@ -0,0 +1,8 @@ +// Rename this file to settings.js and adjust the settings + +window.absenzenmanagement = window.absenzenmanagement || {}; + +window.absenzenmanagement.settings = { + // API base URL without trailing slash + apiUrl: 'https://eventotest.api' +};