From 18ea19b900d5d376e1186536e589cd8048ebf63e Mon Sep 17 00:00:00 2001 From: Tobias Schweizer Date: Wed, 16 Jun 2021 16:57:45 +0200 Subject: [PATCH] feat(viewer): resolve geoname id and show name (DSP-1212) (#305) * feat(viewer): resolve geoname id and show name * feat(viewer): remove label input from geoname val comp * test(viewer): add specs to geoname service * test(app): mock component --- .../viewer/services/geoname.service.spec.ts | 94 +++++++++++++++++++ .../lib/viewer/services/geoname.service.ts | 38 ++++++++ .../geoname-value.component.html | 2 +- .../geoname-value.component.spec.ts | 53 +++++------ .../geoname-value/geoname-value.component.ts | 16 +++- .../viewer-playground.component.spec.ts | 16 +++- src/config/config.dev.json | 3 +- src/config/config.prod.json | 3 +- 8 files changed, 190 insertions(+), 35 deletions(-) create mode 100644 projects/dsp-ui/src/lib/viewer/services/geoname.service.spec.ts create mode 100644 projects/dsp-ui/src/lib/viewer/services/geoname.service.ts diff --git a/projects/dsp-ui/src/lib/viewer/services/geoname.service.spec.ts b/projects/dsp-ui/src/lib/viewer/services/geoname.service.spec.ts new file mode 100644 index 000000000..4872b5d9a --- /dev/null +++ b/projects/dsp-ui/src/lib/viewer/services/geoname.service.spec.ts @@ -0,0 +1,94 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { GeonameService } from './geoname.service'; +import { AppInitService } from '../../core'; + +describe('GeonameService', () => { + let service: GeonameService; + let httpTestingController: HttpTestingController; + + const appInitSpy = { + config: { + geonameToken: 'token' + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { + provide: AppInitService, + useValue: appInitSpy + } + ] + }); + + service = TestBed.inject(GeonameService); + httpTestingController = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should resolve a given geoname id', done => { + + service.resolveGeonameID('2661604').subscribe( + name => { + expect(name).toEqual('Basel'); + done(); + } + ); + + const httpRequest = httpTestingController.expectOne('https://ws.geonames.net/getJSON?geonameId=2661604&username=token&style=short'); + + expect(httpRequest.request.method).toEqual('GET'); + + const expectedResponse = { name: 'Basel' }; + + httpRequest.flush(expectedResponse); + + }); + + it('should use the given geoname id as a fallback value if the requests fails', done => { + + service.resolveGeonameID('2661604').subscribe( + name => { + expect(name).toEqual('2661604'); + done(); + } + ); + + const httpRequest = httpTestingController.expectOne('https://ws.geonames.net/getJSON?geonameId=2661604&username=token&style=short'); + + expect(httpRequest.request.method).toEqual('GET'); + + const mockErrorResponse = {status: 400, statusText: 'Bad Request'}; + + httpRequest.flush(mockErrorResponse); + + }); + + it('should use the given geoname id as a fallback value if the requests response does not contain the expected information', done => { + + service.resolveGeonameID('2661604').subscribe( + name => { + expect(name).toEqual('2661604'); + done(); + } + ); + + const httpRequest = httpTestingController.expectOne('https://ws.geonames.net/getJSON?geonameId=2661604&username=token&style=short'); + + expect(httpRequest.request.method).toEqual('GET'); + + const expectedResponse = { place: 'Basel' }; + + httpRequest.flush(expectedResponse); + + }); + +}); diff --git a/projects/dsp-ui/src/lib/viewer/services/geoname.service.ts b/projects/dsp-ui/src/lib/viewer/services/geoname.service.ts new file mode 100644 index 000000000..c65a47782 --- /dev/null +++ b/projects/dsp-ui/src/lib/viewer/services/geoname.service.ts @@ -0,0 +1,38 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AppInitService } from '../../core'; + +@Injectable({ + providedIn: 'root' +}) +export class GeonameService { + + constructor( + private readonly _http: HttpClient, + private _appInitService: AppInitService + ) { + } + + resolveGeonameID(id: string): Observable { + + return this._http.get('https://ws.geonames.net/getJSON?geonameId=' + id + '&username=' + this._appInitService.config['geonameToken'] + '&style=short').pipe( + map( + (geo: { name: string }) => { + + if (!('name' in geo)) { + throw 'no name property'; + } + + return geo.name; + } + ), + catchError(error => { + // an error occurred, just return the id + return of(id); + }) + ); + + } +} diff --git a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.html b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.html index 03903becd..83edecd20 100644 --- a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.html +++ b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.html @@ -1,6 +1,6 @@ - {{ label ? label : valueFormControl.value}} + {{ $geonameLabel | async }} {{ commentFormControl.value }} diff --git a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.spec.ts b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.spec.ts index f2edb4d48..df52e1e43 100644 --- a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.spec.ts +++ b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.spec.ts @@ -7,6 +7,8 @@ import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { CreateGeonameValue, MockResource, ReadGeonameValue, UpdateGeonameValue } from '@dasch-swiss/dsp-js'; import { GeonameValueComponent } from './geoname-value.component'; +import { GeonameService } from '../../services/geoname.service'; +import { of } from 'rxjs'; @@ -15,7 +17,7 @@ import { GeonameValueComponent } from './geoname-value.component'; */ @Component({ template: ` - ` + ` }) class TestHostDisplayValueComponent implements OnInit { @@ -25,8 +27,6 @@ class TestHostDisplayValueComponent implements OnInit { mode: 'read' | 'update' | 'create' | 'search'; - label: string; - ngOnInit() { MockResource.getTestThing().subscribe(res => { @@ -63,6 +63,9 @@ class TestHostCreateValueComponent implements OnInit { describe('GeonameValueComponent', () => { beforeEach(waitForAsync(() => { + + const mockGeonameService = jasmine.createSpyObj('GeonameService', ['resolveGeonameID']); + TestBed.configureTestingModule({ declarations: [ GeonameValueComponent, @@ -75,6 +78,10 @@ describe('GeonameValueComponent', () => { BrowserAnimationsModule, MatIconModule ], + providers: [{ + provide: GeonameService, + useValue: mockGeonameService + }] }) .compileComponents(); })); @@ -89,6 +96,10 @@ describe('GeonameValueComponent', () => { let valueReadModeNativeElement; beforeEach(() => { + const geonameServiceMock = TestBed.inject(GeonameService) as jasmine.SpyObj; + + geonameServiceMock.resolveGeonameID.and.returnValue(of('Basel')); + testHostFixture = TestBed.createComponent(TestHostDisplayValueComponent); testHostComponent = testHostFixture.componentInstance; testHostFixture.detectChanges(); @@ -104,13 +115,15 @@ describe('GeonameValueComponent', () => { it('should display an existing value', () => { + const geonameServiceMock = TestBed.inject(GeonameService) as jasmine.SpyObj; + expect(testHostComponent.inputValueComponent.displayValue.geoname).toEqual('2661604'); expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); expect(testHostComponent.inputValueComponent.mode).toEqual('read'); - expect(valueReadModeNativeElement.innerText).toEqual('2661604'); + expect(valueReadModeNativeElement.innerText).toEqual('Basel'); const anchorDebugElement = valueReadModeDebugElement.query(By.css('a')); expect(anchorDebugElement.nativeElement).toBeDefined(); @@ -118,29 +131,9 @@ describe('GeonameValueComponent', () => { expect(anchorDebugElement.attributes.href).toEqual('https://www.geonames.org/2661604'); expect(anchorDebugElement.attributes.target).toEqual('_blank'); - }); - - it('should display an existing value with a label', () => { - - testHostComponent.label = 'testlabel'; - - testHostFixture.detectChanges(); + expect(geonameServiceMock.resolveGeonameID).toHaveBeenCalledOnceWith('2661604'); - expect(testHostComponent.inputValueComponent.displayValue.geoname).toEqual('2661604'); - - expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); - - expect(testHostComponent.inputValueComponent.mode).toEqual('read'); - - expect(valueReadModeNativeElement.innerText).toEqual('testlabel'); - - const anchorDebugElement = valueReadModeDebugElement.query(By.css('a')); - expect(anchorDebugElement.nativeElement).toBeDefined(); - - expect(anchorDebugElement.attributes.href).toEqual('https://www.geonames.org/2661604'); - expect(anchorDebugElement.attributes.target).toEqual('_blank'); - - }); + }); it('should make an existing value editable', () => { @@ -233,6 +226,10 @@ describe('GeonameValueComponent', () => { it('should set a new display value', () => { + const geonameServiceMock = TestBed.inject(GeonameService) as jasmine.SpyObj; + + geonameServiceMock.resolveGeonameID.and.returnValue(of('Terra Linda High School')); + const newStr = new ReadGeonameValue(); newStr.geoname = '5401678'; @@ -242,10 +239,12 @@ describe('GeonameValueComponent', () => { testHostFixture.detectChanges(); - expect(valueReadModeNativeElement.innerText).toEqual('5401678'); + expect(valueReadModeNativeElement.innerText).toEqual('Terra Linda High School'); expect(testHostComponent.inputValueComponent.form.valid).toBeTruthy(); + expect(geonameServiceMock.resolveGeonameID).toHaveBeenCalledWith('5401678'); + }); it('should unsubscribe when destroyed', () => { diff --git a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.ts b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.ts index a627188f9..276b14eaa 100644 --- a/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.ts +++ b/projects/dsp-ui/src/lib/viewer/values/geoname-value/geoname-value.component.ts @@ -1,10 +1,11 @@ import { Component, Inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { CreateGeonameValue, ReadGeonameValue, UpdateGeonameValue } from '@dasch-swiss/dsp-js'; -import { Subscription } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { BaseValueComponent } from '../base-value.component'; import { CustomRegex } from '../custom-regex'; import { ValueErrorStateMatcher } from '../value-error-state-matcher'; +import { GeonameService } from '../../services/geoname.service'; // https://stackoverflow.com/questions/45661010/dynamic-nested-reactive-form-expressionchangedafterithasbeencheckederror const resolvedPromise = Promise.resolve(null); @@ -16,7 +17,6 @@ const resolvedPromise = Promise.resolve(null); }) export class GeonameValueComponent extends BaseValueComponent implements OnInit, OnChanges, OnDestroy { @Input() displayValue?: ReadGeonameValue; - @Input() label?: string; valueFormControl: FormControl; commentFormControl: FormControl; @@ -27,7 +27,9 @@ export class GeonameValueComponent extends BaseValueComponent implements OnInit, matcher = new ValueErrorStateMatcher(); customValidators = [Validators.pattern(CustomRegex.GEONAME_REGEX)]; - constructor(@Inject(FormBuilder) private _fb: FormBuilder) { + $geonameLabel: Observable; + + constructor(@Inject(FormBuilder) private _fb: FormBuilder, private _geonameService: GeonameService) { super(); } @@ -60,6 +62,10 @@ export class GeonameValueComponent extends BaseValueComponent implements OnInit, this.resetFormControl(); + if (this.mode === 'read') { + this.$geonameLabel = this._geonameService.resolveGeonameID(this.valueFormControl.value); + } + resolvedPromise.then(() => { // add form to the parent form group this.addToParentFormGroup(this.formName, this.form); @@ -71,6 +77,10 @@ export class GeonameValueComponent extends BaseValueComponent implements OnInit, // resets values and validators in form controls when input displayValue or mode changes // at the first call of ngOnChanges, form control elements are not initialized yet this.resetFormControl(); + + if (this.mode === 'read' && this.valueFormControl !== undefined) { + this.$geonameLabel = this._geonameService.resolveGeonameID(this.valueFormControl.value); + } } ngOnDestroy(): void { diff --git a/src/app/viewer-playground/viewer-playground.component.spec.ts b/src/app/viewer-playground/viewer-playground.component.spec.ts index 94cc02499..473373899 100644 --- a/src/app/viewer-playground/viewer-playground.component.spec.ts +++ b/src/app/viewer-playground/viewer-playground.component.spec.ts @@ -13,6 +13,18 @@ import { DspApiConnectionToken, DspViewerModule, UserService } from '@dasch-swis import { ViewerPlaygroundComponent } from './viewer-playground.component'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: `dsp-resource-view`, + template: `` +}) +class TestResourveViewComponent { + + @Input() iri: string; + + @Input() showToolbar: boolean; +} describe('ViewerPlaygroundComponent', () => { let component: ViewerPlaygroundComponent; @@ -33,10 +45,10 @@ describe('ViewerPlaygroundComponent', () => { TestBed.configureTestingModule({ declarations: [ - ViewerPlaygroundComponent + ViewerPlaygroundComponent, + TestResourveViewComponent ], imports: [ - DspViewerModule, MatIconModule, MatTooltipModule ], diff --git a/src/config/config.dev.json b/src/config/config.dev.json index 4ce4d92cd..30d75947d 100644 --- a/src/config/config.dev.json +++ b/src/config/config.dev.json @@ -5,5 +5,6 @@ "apiPath": "", "jsonWebToken": "", "logErrors": true, - "sipiUrl": "http://localhost:1024/" + "sipiUrl": "http://localhost:1024/", + "geonameToken": "knora" } diff --git a/src/config/config.prod.json b/src/config/config.prod.json index 869fea73a..78c392f97 100644 --- a/src/config/config.prod.json +++ b/src/config/config.prod.json @@ -5,5 +5,6 @@ "apiPath": "", "jsonWebToken": "", "logErrors": false, - "sipiUrl": "https://iiif.dasch.swiss:1024/" + "sipiUrl": "https://iiif.dasch.swiss:1024/", + "geonameToken": "knora" }