diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index 2c3b3c970e64e3c185933ae2cd8f57515fe97bd8..062c160c5fe4f3fa84539d5e9665a2b5c299f5e5 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -15,6 +15,7 @@ import * as searchMultiple from './store/reducers/search-multiple.reducer'; import * as samp from './store/reducers/samp.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; import * as detail from './store/reducers/detail.reducer'; +import * as svomJsonKw from './store/reducers/svom-json-kw.reducer'; /** * Interface for instance state. @@ -26,7 +27,8 @@ export interface State { searchMultiple: searchMultiple.State, samp: samp.State, coneSearch: coneSearch.State - detail: detail.State + detail: detail.State, + svomJsonKw: svomJsonKw.State } const reducers = { @@ -34,7 +36,8 @@ const reducers = { searchMultiple: searchMultiple.searchMultipleReducer, samp: samp.sampReducer, coneSearch: coneSearch.coneSearchReducer, - detail: detail.detailReducer + detail: detail.detailReducer, + svomJsonKw: svomJsonKw.svomJsonKwReducer }; export const instanceReducer = combineReducers(reducers); diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html index 6fa2b430f3cceae54c6775ccf9e9a6fa3fe1e283..d7fa428fe7c4f09e812f595287699e40f4e9534f 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html @@ -149,6 +149,9 @@ [label]="attribute.form_label" [criterion]="getCriterion(attribute.id)" [criteriaList]="criteriaList" + [svomKeywords]="svomKeywords" + (selectSvomAcronym)=selectSvomAcronym.emit($event) + (resetSvomKeywords)="resetSvomKeywords.emit()" (addCriterion)="emitAdd($event)" (deleteCriterion)="emitDelete($event)"> </app-svom-json-kw-criteria> diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.spec.ts b/client/src/app/instance/search/components/criteria/criteria-by-family.component.spec.ts index d019066be3820edf7c619ca5bb5d8bcc315df93f..946737df076868a875e58eb60225d2e6f9c2989a 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.spec.ts +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CriteriaByFamilyComponent } from './criteria-by-family.component'; import { Option } from '../../../../metamodel/models'; -import { Criterion, FieldCriterion } from '../../../store/models'; +import { Criterion, FieldCriterion, SvomKeyword } from '../../../store/models'; describe('[Instance][Search][Component][Criteria] CriteriaByFamilyComponent', () => { @Component({ selector: 'app-field', template: '' }) @@ -123,6 +123,15 @@ describe('[Instance][Search][Component][Criteria] CriteriaByFamilyComponent', () @Input() criterion: Criterion; } + @Component({ selector: 'app-svom-json-kw-criteria', template: '' }) + class SvomJsonStubKwComponent { + @Input() id: number; + @Input() label: string; + @Input() criterion: Criterion; + @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; + } + let component: CriteriaByFamilyComponent; let fixture: ComponentFixture<CriteriaByFamilyComponent>; @@ -142,7 +151,8 @@ describe('[Instance][Search][Component][Criteria] CriteriaByFamilyComponent', () BetweenDateStubComponent, TimeStubComponent, DatetimeStubComponent, - JsonStubComponent + JsonStubComponent, + SvomJsonStubKwComponent ] }); fixture = TestBed.createComponent(CriteriaByFamilyComponent); diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts index ebfbed20ab4eb9279ea140e8c4bcaf0f69553529..e64b8eb0207828546d82bb13bd1a1f19104759c2 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts @@ -9,7 +9,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { Criterion } from '../../../store/models'; +import { Criterion, SvomKeyword } from '../../../store/models'; import { Attribute, Option } from 'src/app/metamodel/models'; /** @@ -24,6 +24,9 @@ import { Attribute, Option } from 'src/app/metamodel/models'; export class CriteriaByFamilyComponent { @Input() attributeList: Attribute[]; @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; + @Output() selectSvomAcronym: EventEmitter<string> = new EventEmitter(); + @Output() resetSvomKeywords: EventEmitter<{}> = new EventEmitter(); @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); diff --git a/client/src/app/instance/search/components/criteria/criteria-tabs.component.html b/client/src/app/instance/search/components/criteria/criteria-tabs.component.html index fa9d3aef3286c6ac5d3fb9cdd32279b4c00f2d3b..d659ea65ae3b65a642451ce7f58b955ff7a0e342 100644 --- a/client/src/app/instance/search/components/criteria/criteria-tabs.component.html +++ b/client/src/app/instance/search/components/criteria/criteria-tabs.component.html @@ -16,6 +16,9 @@ <app-criteria-by-family [attributeList]="attributeList | attributeListByFamily:family.id" [criteriaList]="criteriaList" + [svomKeywords]="svomKeywords" + (selectSvomAcronym)="selectSvomAcronym.emit($event)" + (resetSvomKeywords)="resetSvomKeywords.emit()" (addCriterion)="emitAdd($event)" (deleteCriterion)="emitDelete($event)"> </app-criteria-by-family> diff --git a/client/src/app/instance/search/components/criteria/criteria-tabs.component.spec.ts b/client/src/app/instance/search/components/criteria/criteria-tabs.component.spec.ts index b2557821f8e1361874a432af5b41a053f767890d..11b0bca600500357f1a65c3d94faf70aabdf8649 100644 --- a/client/src/app/instance/search/components/criteria/criteria-tabs.component.spec.ts +++ b/client/src/app/instance/search/components/criteria/criteria-tabs.component.spec.ts @@ -6,7 +6,7 @@ import { AccordionModule } from 'ngx-bootstrap/accordion'; import { CriteriaTabsComponent } from './criteria-tabs.component'; import { Attribute } from '../../../../metamodel/models'; -import { Criterion, FieldCriterion } from '../../../store/models'; +import { Criterion, FieldCriterion, SvomKeyword } from '../../../store/models'; import { AttributeListByFamilyPipe } from '../../../../shared/pipes/attribute-list-by-family.pipe'; describe('[Instance][Search][Component][Criteria] CriteriaTabsComponent', () => { @@ -14,6 +14,7 @@ describe('[Instance][Search][Component][Criteria] CriteriaTabsComponent', () => class CriteriaByFamilyStubComponent { @Input() attributeList: Attribute[]; @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; } let component: CriteriaTabsComponent; diff --git a/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts b/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts index a7c45275faa2fd646154798bc4b8d6d0f1ec5f10..7ed5b94db60583441a66718f4b014ec8a350f8e5 100644 --- a/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts +++ b/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts @@ -9,7 +9,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { Criterion } from '../../../store/models'; +import { Criterion, SvomKeyword } from '../../../store/models'; import { CriteriaFamily, Attribute } from 'src/app/metamodel/models'; /** @@ -25,6 +25,9 @@ export class CriteriaTabsComponent { @Input() attributeList: Attribute[]; @Input() criteriaFamilyList: CriteriaFamily[]; @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; + @Output() selectSvomAcronym: EventEmitter<string> = new EventEmitter(); + @Output() resetSvomKeywords: EventEmitter<{}> = new EventEmitter(); @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.html b/client/src/app/instance/search/components/criteria/search-type/json.component.html index 8d69dac7d08c4e7d15d27433e8cda7129e05ccfb..3780ed1fbc2f24f1194b99e89a304c72005c7908 100644 --- a/client/src/app/instance/search/components/criteria/search-type/json.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/json.component.html @@ -8,14 +8,13 @@ </div> <div class="w-100 d-block d-sm-none"></div> <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> - <select class="custom-select" id="operator" name="operator" formControlName="operator"> - <option></option> - <option value="eq">=</option> - <option value="gt">></option> - <option value="gte">>=</option> - <option value="lt"><</option> - <option value="lte"><=</option> - </select> + <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> + <ng-option value="eq">=</ng-option > + <ng-option value="gt">></ng-option > + <ng-option value="gte">>=</ng-option > + <ng-option value="lt"><</ng-option > + <ng-option value="lte"><=</ng-option > + </ng-select> </div> <div class="w-100 d-block d-sm-none"></div> <div class="col"> diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts index 6941d1178975af0f51ce2abd9358ab0d873204ed..673739f3f74f1f5c918828186d1bee27709d0c07 100644 --- a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts +++ b/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts @@ -2,6 +2,8 @@ import { Component, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NgSelectModule } from '@ng-select/ng-select'; + import { JsonComponent } from './json.component'; import { JsonCriterion } from '../../../../store/models/criterion'; @@ -35,7 +37,8 @@ describe('[Instance][Search][Component][Criteria][SearchType] JsonComponent', () ], imports: [ FormsModule, - ReactiveFormsModule + ReactiveFormsModule, + NgSelectModule ] }); testHostFixture = TestBed.createComponent(TestHostComponent); diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html index 1f94ffca48497a3513aa1534b79c633ae053c241..7257b9970af74837ca086afe11c8d033c5b5045a 100644 --- a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html @@ -4,26 +4,20 @@ <label>{{ label }}</label> <div class="row"> <div class="col mb-1 mb-sm-0"> - <input *ngIf="!acronym" class="form-control" name="ext" placeholder="Ext" autocomplete="off" formControlName="ext"> - <select *ngIf="acronym" class="form-control" name="ext" formControlName="ext" (change)="extOnChange()"> - <option></option> - <option *ngFor="let ext of exts" [ngValue]="ext">{{ ext }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col mb-1 mb-sm-0 pl-sm-0 pl-md-0 pl-lg-0"> - <input *ngIf="!acronym" class="form-control" id="keyword" name="keyword" placeholder="Keyword" autocomplete="off" formControlName="keyword"> + <input *ngIf="svomKeywords.length < 1" class="form-control" id="path" name="path" placeholder="Path" autocomplete="off" formControlName="path"> + <ng-select *ngIf="svomKeywords.length > 0" [multiple]="false" [hideSelected]="true" placeholder="Select svom product keyword" class="ng-select-custom" formControlName="path"> + <ng-option *ngFor="let svomKeyword of svomKeywords" [value]="getKeywordValue(svomKeyword)">{{ svomKeyword.extension }}/{{ svomKeyword.name }}</ng-option> + </ng-select> </div> <div class="w-100 d-block d-sm-none"></div> <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> - <select class="custom-select" id="operator" name="operator" formControlName="operator"> - <option></option> - <option value="eq">=</option> - <option value="gt">></option> - <option value="gte">>=</option> - <option value="lt"><</option> - <option value="lte"><=</option> - </select> + <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> + <ng-option value="eq">=</ng-option > + <ng-option value="gt">></ng-option > + <ng-option value="gte">>=</ng-option > + <ng-option value="lt"><</ng-option > + <ng-option value="lte"><=</ng-option > + </ng-select> </div> <div class="w-100 d-block d-sm-none"></div> <div class="col"> diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..05f55eec0e209ff39dc6559c31db1c4b60a81554 --- /dev/null +++ b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts @@ -0,0 +1,93 @@ +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { NgSelectModule } from '@ng-select/ng-select'; + +import { SvomJsonKwComponent } from './svom-json-kw.component'; +import { Criterion, JsonCriterion, SvomKeyword } from '../../../../store/models'; + +describe('[Instance][Search][Component][Criteria][SearchType] SvomJsonKwComponent', () => { + @Component({ + selector: `app-host`, + template: ` + <app-svom-json-kw-criteria + [id]="id" + [label]="label" + [criterion]="criterion" + [criteriaList]="criteriaList" + [svomKeywords]="svomKeywords"> + </app-svom-json-kw-criteria >` + }) + class TestHostComponent { + @ViewChild(SvomJsonKwComponent, { static: false }) + public testedComponent: SvomJsonKwComponent; + public id: number = undefined; + public label: string = undefined; + public criterion: JsonCriterion = undefined; + public criteriaList: Criterion[] = []; + public svomKeywords: SvomKeyword[] = []; + } + + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture<TestHostComponent>; + let testedComponent: SvomJsonKwComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + SvomJsonKwComponent, + TestHostComponent + ], + imports: [ + FormsModule, + ReactiveFormsModule, + NgSelectModule + ] + }); + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + testedComponent = testHostComponent.testedComponent; + }); + + it('should create the component', () => { + expect(testedComponent).toBeTruthy(); + }); + + it('should call ngOnChanges and apply changes', () => { + const spy = jest.spyOn(testedComponent, 'ngOnChanges'); + testHostComponent.criterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; + testHostFixture.detectChanges(); + expect(testedComponent.form.controls.path.value).toEqual('myPath'); + expect(testedComponent.form.controls.operator.value).toEqual('myOperator'); + expect(testedComponent.form.controls.value.value).toEqual('myValue'); + expect(testedComponent.form.disabled).toBeTruthy(); + testHostComponent.criterion = undefined; + testHostFixture.detectChanges(); + expect(testedComponent.form.controls.path.value).toBeNull(); + expect(testedComponent.form.controls.operator.value).toBeNull(); + expect(testedComponent.form.controls.value.value).toBeNull(); + expect(testedComponent.form.enabled).toBeTruthy(); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('raises the add criterion event when clicked', () => { + testedComponent.id = 1; + testedComponent.form.controls.path.setValue('myPath'); + testedComponent.form.controls.operator.setValue('myOperator'); + testedComponent.form.controls.value.setValue('myValue'); + const expectedCriterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; + testedComponent.addCriterion.subscribe((event: JsonCriterion) => expect(event).toEqual(expectedCriterion)); + testedComponent.emitAdd(); + }); + + it('Calculates the value of ng-option', () => { + expect(testedComponent.getKeywordValue({ + extension: 'PrimaryHDU', + name: 'CARD', + data_type: 'string', + default: '' + })).toEqual('PrimaryHDU,CARD'); + }); +}); diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts index 57c2f4f1fcf7ea5c2f9c1eeee1c09202da19d3a2..9feb30f53132906618468ce6fe3452a4b97b4ec2 100644 --- a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts @@ -8,11 +8,9 @@ */ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnChanges, SimpleChanges } from '@angular/core'; -import {Â HttpClient } from '@angular/common/http'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { JsonCriterion, Criterion } from 'src/app/instance/store/models'; -import { AppConfigService } from 'src/app/app-config.service'; +import { JsonCriterion, Criterion, SvomKeyword } from 'src/app/instance/store/models'; @Component({ selector: 'app-svom-json-kw-criteria', @@ -24,19 +22,14 @@ export class SvomJsonKwComponent implements OnChanges { @Input() label: string; @Input() criterion: Criterion; @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; + @Output() selectSvomAcronym: EventEmitter<string> = new EventEmitter(); + @Output() resetSvomKeywords: EventEmitter<{}> = new EventEmitter(); @Output() addCriterion: EventEmitter<JsonCriterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - acronym = false; - keywordsSearchable = null; - exts: string[] = []; - keywords: string[] = []; - - public constructor(private httpClient:Â HttpClient, private config: AppConfigService) { } - public form = new FormGroup({ - ext: new FormControl('', [Validators.required]), - keyword: new FormControl('', [Validators.required]), + path: new FormControl('', [Validators.required]), operator: new FormControl('', [Validators.required]), value: new FormControl('', [Validators.required]) }); @@ -52,25 +45,23 @@ export class SvomJsonKwComponent implements OnChanges { this.form.reset(); } - if (changes.criteriaList && changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3)) { - this.acronym = true; - this.getKwSearchable(changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3).value); + if (changes.criteriaList && this.svomKeywords.length < 1 && changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3)) { + this.selectSvomAcronym.emit(changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3).value); } - } - getKwSearchable(acronym: string) { - this.httpClient.get(`${this.config.apiUrl}/search/sp_cards?a=8&c=1::eq::${acronym}`).subscribe(data => { - this.keywordsSearchable = data[0].search_kw; - this.exts = [...new Set<string>(this.keywordsSearchable.map(item => item.extension))]; - console.log(data); - /* this.exts = this.keywordsSearchable - .map(item => item.extension) - .filter((value, index, self) => self.indexOf(value.) === index); */ - }); + if (changes.criteriaList && this.svomKeywords.length > 0 && !changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3)) { + this.resetSvomKeywords.emit(); + } } - extOnChange() { - + /** + * Transform a SVOM json Keyword to as path value (anis json search) + * + * @param svomKeyword Keyword selected by user + * @returns string path value + */ + getKeywordValue(svomKeyword: SvomKeyword): string { + return `${svomKeyword.extension},${svomKeyword.name}` } /** @@ -79,12 +70,7 @@ export class SvomJsonKwComponent implements OnChanges { * @fires EventEmitter<JsonCriterion> */ emitAdd(): void { - const formValue = { - path: `${this.form.controls.ext.value},${this.form.controls.keyword.value}`, - operator: this.form.controls.operator.value, - value: this.form.controls.value.value - } - const js = { id: this.id, type: 'json', ...formValue }; + const js = { id: this.id, type: 'json', ...this.form.value }; this.addCriterion.emit(js); } } diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html index 0afddb0a5e155f3d9a8aa4784abb75de57670e2b..c24a1901fe0ca6198428e4f0be0c9e85933d1177 100644 --- a/client/src/app/instance/search/containers/criteria.component.html +++ b/client/src/app/instance/search/containers/criteria.component.html @@ -22,6 +22,9 @@ [attributeList]="attributeList | async | sortByCriteriaDisplay" [criteriaFamilyList]="criteriaFamilyList | async" [criteriaList]="criteriaList | async" + [svomKeywords]="svomKeywords | async" + (selectSvomAcronym)="selectSvomAcronym($event)" + (resetSvomKeywords)="resetSvomKeywords()" (addCriterion)="addCriterion($event)" (deleteCriterion)="deleteCriterion($event)"> </app-criteria-tabs> diff --git a/client/src/app/instance/search/containers/criteria.component.spec.ts b/client/src/app/instance/search/containers/criteria.component.spec.ts index a1c1e2eb5202c9b8c9b3417add3eedafcff32595..035339c21739e54e00efae74b15c2363a7560472 100644 --- a/client/src/app/instance/search/containers/criteria.component.spec.ts +++ b/client/src/app/instance/search/containers/criteria.component.spec.ts @@ -6,7 +6,7 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { CriteriaComponent } from './criteria.component'; import { Attribute, CriteriaFamily, Dataset, OutputCategory, OutputFamily } from '../../../metamodel/models'; -import { ConeSearch, Criterion, Resolver, SearchQueryParams } from '../../store/models'; +import { ConeSearch, Criterion, Resolver, SearchQueryParams, SvomKeyword } from '../../store/models'; import { SortByCriteriaDisplayPipe } from '../pipes/sort-by-criteria-display.pipe'; import * as searchActions from '../../store/actions/search.actions'; import { AbstractSearchComponent } from './abstract-search.component'; @@ -31,6 +31,7 @@ describe('[Instance][Search][Container] CriteriaComponent', () => { @Input() attributeList: Attribute[]; @Input() criteriaFamilyList: CriteriaFamily[]; @Input() criteriaList: Criterion[]; + @Input() svomKeywords: SvomKeyword[]; } @Component({ selector: 'app-summary', template: '' }) diff --git a/client/src/app/instance/search/containers/criteria.component.ts b/client/src/app/instance/search/containers/criteria.component.ts index 2530489e8fbbbe638e656bf04bab072526cf232f..a73921a9fd50efacaf32576f09a219d06e8cf258 100644 --- a/client/src/app/instance/search/containers/criteria.component.ts +++ b/client/src/app/instance/search/containers/criteria.component.ts @@ -13,10 +13,12 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { AbstractSearchComponent } from './abstract-search.component'; -import { ConeSearch, Criterion, Resolver } from '../../store/models'; +import { ConeSearch, Criterion, Resolver, SvomKeyword } from '../../store/models'; import * as searchActions from '../../store/actions/search.actions'; import * as coneSearchActions from '../../store/actions/cone-search.actions'; import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; +import * as svomJsonKwActions from '../../store/actions/svom-json-kw.actions'; +import * as svomJsonKwSelector from '../../store/selectors/svom-json-kw.selector'; /** * @class @@ -30,12 +32,14 @@ export class CriteriaComponent extends AbstractSearchComponent { public resolver: Observable<Resolver>; public resolverIsLoading: Observable<boolean>; public resolverIsLoaded: Observable<boolean>; + public svomKeywords: Observable<SvomKeyword[]>; constructor(protected store: Store<{ }>) { super(store); this.resolver = this.store.select(coneSearchSelector.selectResolver); this.resolverIsLoading = this.store.select(coneSearchSelector.selectResolverIsLoading); this.resolverIsLoaded = this.store.select(coneSearchSelector.selectResolverIsLoaded); + this.svomKeywords = this.store.select(svomJsonKwSelector.selectSvomKeywords); } ngOnInit(): void { @@ -88,4 +92,12 @@ export class CriteriaComponent extends AbstractSearchComponent { retrieveCoordinates(name: string): void { this.store.dispatch(coneSearchActions.retrieveCoordinates({ name })); } + + selectSvomAcronym(acronymSelected: string): void { + this.store.dispatch(svomJsonKwActions.selectAcronym({ acronymSelected })); + } + + resetSvomKeywords(): void { + Promise.resolve(null).then(() => this.store.dispatch(svomJsonKwActions.resetKw())); + } } diff --git a/client/src/app/instance/store/actions/svom-json-kw.actions.ts b/client/src/app/instance/store/actions/svom-json-kw.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ce485e9783ec82278d2d63a98a4fd2f089bde48 --- /dev/null +++ b/client/src/app/instance/store/actions/svom-json-kw.actions.ts @@ -0,0 +1,18 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +import { SvomKeyword } from '../models'; + +export const resetKw = createAction('[SVOM Json Kw] Reset Kw'); +export const selectAcronym = createAction('[SVOM Json Kw] Select Acronym', props<{ acronymSelected: string }>()); +export const loadKwSearchable = createAction('[SVOM Json Kw] Load Kw Searchable'); +export const loadKwSearchableSuccess = createAction('[SVOM Json Kw] Load Kw Searchable Success', props<{ svomKeywords: SvomKeyword[] }>()); +export const loadKwSearchableFail = createAction('[SVOM Json Kw] Load Kw Searchable Fail'); \ No newline at end of file diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 582dd51102f0f5b76cdf2db4ff4fd0e3e36307b6..69b9a3b26cc0cd09cc16e974cbb4ddc4102571b9 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -3,11 +3,13 @@ import { SearchEffects } from './search.effects'; import { SearchMultipleEffects } from './search-multiple.effects'; import { ConeSearchEffects } from './cone-search.effects'; import { DetailEffects } from './detail.effects'; +import { SvomJsonKwEffects } from './svom-json-kw.effects'; export const instanceEffects = [ SampEffects, SearchEffects, SearchMultipleEffects, ConeSearchEffects, - DetailEffects + DetailEffects, + SvomJsonKwEffects ]; diff --git a/client/src/app/instance/store/effects/svom-json-kw.effects.spec.ts b/client/src/app/instance/store/effects/svom-json-kw.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed3ca655f9ce922c6c0f93729243897bbb2eaf07 --- /dev/null +++ b/client/src/app/instance/store/effects/svom-json-kw.effects.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; + +import { provideMockActions } from '@ngrx/effects/testing'; +import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; +import { provideMockStore } from '@ngrx/store/testing'; +import { Observable } from 'rxjs'; +import { ToastrService } from 'ngx-toastr'; + +import { SvomJsonKwEffects } from './svom-json-kw.effects'; +import { SvomJsonKwService } from '../services/svom-json-kw.service'; +import * as fromSvomJsonKw from '../reducers/svom-json-kw.reducer'; + +describe('[Instance][Store] SvomJsonKwEffects', () => { + let actions = new Observable(); + let effects: SvomJsonKwEffects; + let metadata: EffectsMetadata<SvomJsonKwEffects>; + let svomJsonKwService: SvomJsonKwService; + let toastr: ToastrService; + const initialState = { + instance: { + svomJsonKw: { ...fromSvomJsonKw.initialState } + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SvomJsonKwEffects, + { provide: SvomJsonKwService, useValue: { loadKwSearchable: jest.fn() }}, + { provide: ToastrService, useValue: { success: jest.fn(), error: jest.fn() }}, + provideMockActions(() => actions), + provideMockStore({ initialState }) + ] + }).compileComponents(); + effects = TestBed.inject(SvomJsonKwEffects); + metadata = getEffectsMetadata(effects); + svomJsonKwService = TestBed.inject(SvomJsonKwService); + toastr = TestBed.inject(ToastrService); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); +}); diff --git a/client/src/app/instance/store/effects/svom-json-kw.effects.ts b/client/src/app/instance/store/effects/svom-json-kw.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6d32c75943ed0bd426e1476dbf21ded68578c5d --- /dev/null +++ b/client/src/app/instance/store/effects/svom-json-kw.effects.ts @@ -0,0 +1,71 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import * as svomJsonKwActions from '../actions/svom-json-kw.actions'; +import * as svomJsonKwSelector from '../selectors/svom-json-kw.selector'; + +import { SvomJsonKwService } from '../services/svom-json-kw.service'; + +/** + * @class + * @classdesc Svom Json Kw effects. + */ +@Injectable() +export class SvomJsonKwEffects { + selectAcronym$ = createEffect((): any => + this.actions$.pipe( + ofType(svomJsonKwActions.selectAcronym), + map(() => svomJsonKwActions.loadKwSearchable()) + ) + ); + + loadKwSearchable$ = createEffect(() => + this.actions$.pipe( + ofType(svomJsonKwActions.loadKwSearchable), + concatLatestFrom(() => this.store.select(svomJsonKwSelector.selectAcronymSelected)), + mergeMap(([action, acronymSelected]) => this.svomJsonKwService.loadKwSearchable(acronymSelected) + .pipe( + map(svomKeywords => svomJsonKwActions.loadKwSearchableSuccess({ svomKeywords })), + catchError(() => of(svomJsonKwActions.loadKwSearchableFail())) + ) + ) + ) + ); + + loadKwSearchableSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(svomJsonKwActions.loadKwSearchableSuccess), + tap(() => { + this.toastr.success('SVOM Json Keywords was loaded successfully and are now available to product criteria', 'SVOM Json Keywords loaded') + }) + ), { dispatch: false} + ); + + addDatabaseFail$ = createEffect(() => + this.actions$.pipe( + ofType(svomJsonKwActions.loadKwSearchableFail), + tap(() => this.toastr.error('Failure to load Keywords', 'SVOM Json Keywords loaded failed')) + ), { dispatch: false} + ); + + constructor( + private actions$: Actions, + private store: Store<{ }>, + private svomJsonKwService: SvomJsonKwService, + private toastr: ToastrService + ) {} +} diff --git a/client/src/app/instance/store/models/index.ts b/client/src/app/instance/store/models/index.ts index 962eede1ad19a550541f007ee3155525a696c59c..2570f1630cacd2de9afd9bda713f92230d6b8f22 100644 --- a/client/src/app/instance/store/models/index.ts +++ b/client/src/app/instance/store/models/index.ts @@ -7,3 +7,4 @@ export * from './cone-search.model'; export * from './resolver.model'; export * from './search-multiple-dataset-length'; export * from './search-multiple-dataset-data'; +export * from './svom-keyword.model'; \ No newline at end of file diff --git a/client/src/app/instance/store/models/svom-keyword.model.ts b/client/src/app/instance/store/models/svom-keyword.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..1fabc699b4d1e4374f5b81f3120652615019ac65 --- /dev/null +++ b/client/src/app/instance/store/models/svom-keyword.model.ts @@ -0,0 +1,6 @@ +export interface SvomKeyword { + data_type: string + default: string + extension: string + name: string +} \ No newline at end of file diff --git a/client/src/app/instance/store/reducers/svom-json-kw.reducer.spec.ts b/client/src/app/instance/store/reducers/svom-json-kw.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5c3ae74ae385531869dbb3dda44ce1cd42d6094 --- /dev/null +++ b/client/src/app/instance/store/reducers/svom-json-kw.reducer.spec.ts @@ -0,0 +1,82 @@ +import * as fromSvomJsonKw from './svom-json-kw.reducer'; +import * as svomJsonKwActions from '../actions/svom-json-kw.actions'; + +describe('[Instance][Store] Svom Json Kw reducer', () => { + it('unknown action should return the default state', () => { + const { initialState } = fromSvomJsonKw; + const action = { type: 'Unknown' }; + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state).toBe(initialState); + }); + + it('resetKw action should return the default state', () => { + const { initialState } = fromSvomJsonKw; + const action = svomJsonKwActions.resetKw(); + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state).toEqual(initialState); + }); + + it('selectAcronym action should change the acronymSelected', () => { + const { initialState } = fromSvomJsonKw; + const action = svomJsonKwActions.selectAcronym({ acronymSelected: 'OBLC_ECL' }); + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state.acronymSelected).toBe('OBLC_ECL'); + expect(state.svomKeywords).toEqual([]); + expect(state.svomKeywordsIsLoading).toBeFalsy(); + expect(state.svomKeywordsIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('loadKwSearchable action should change the svomKeywordsLoading and svomKeywordsLoaded', () => { + const { initialState } = fromSvomJsonKw; + const action = svomJsonKwActions.loadKwSearchable(); + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state.acronymSelected).toBeNull(); + expect(state.svomKeywords).toEqual([]); + expect(state.svomKeywordsIsLoading).toBeTruthy(); + expect(state.svomKeywordsIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('loadKwSearchableSuccess action should change the svomKeywords, svomKeywordsLoading and svomKeywordsLoaded', () => { + const { initialState } = fromSvomJsonKw; + const svomKeywords = [ + { + data_type: 'string', + default: '', + extension: 'PrimaryHDU', + name: 'CARD' + }, + { + data_type: 'string', + default: '', + extension: 'PrimaryHDU', + name: 'TIME' + } + ]; + const action = svomJsonKwActions.loadKwSearchableSuccess({ svomKeywords }); + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state.acronymSelected).toBeNull(); + expect(state.svomKeywords).toEqual(svomKeywords); + expect(state.svomKeywordsIsLoading).toBeFalsy(); + expect(state.svomKeywordsIsLoaded).toBeTruthy(); + expect(state).not.toBe(initialState); + }); + + it('loadKwSearchableFail action should change the svomKeywordsLoading and svomKeywordsLoaded', () => { + const { initialState } = fromSvomJsonKw; + const action = svomJsonKwActions.loadKwSearchableFail(); + const state = fromSvomJsonKw.svomJsonKwReducer(initialState, action); + + expect(state.acronymSelected).toBeNull(); + expect(state.svomKeywords).toEqual([]); + expect(state.svomKeywordsIsLoading).toBeFalsy(); + expect(state.svomKeywordsIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); +}); diff --git a/client/src/app/instance/store/reducers/svom-json-kw.reducer.ts b/client/src/app/instance/store/reducers/svom-json-kw.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..37c8526080be9f97eaa006dab18595950c64cb4c --- /dev/null +++ b/client/src/app/instance/store/reducers/svom-json-kw.reducer.ts @@ -0,0 +1,64 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; + +import { SvomKeyword } from '../models'; +import * as svomJsonKwActions from '../actions/svom-json-kw.actions'; + +/** + * Interface for svom json kw state. + * + * @interface State + */ +export interface State { + acronymSelected: string; + svomKeywords: SvomKeyword[]; + svomKeywordsIsLoading: boolean; + svomKeywordsIsLoaded: boolean; +} + +export const initialState: State = { + acronymSelected: null, + svomKeywords: [], + svomKeywordsIsLoading: false, + svomKeywordsIsLoaded: false +}; + +export const svomJsonKwReducer = createReducer( + initialState, + on(svomJsonKwActions.resetKw, () => ({ + ...initialState + })), + on(svomJsonKwActions.selectAcronym, (state, { acronymSelected }) => ({ + ...state, + acronymSelected + })), + on(svomJsonKwActions.loadKwSearchable, (state) => ({ + ...state, + svomKeywordsIsLoading: true, + svomKeywordsIsLoaded: false + })), + on(svomJsonKwActions.loadKwSearchableSuccess, (state, { svomKeywords }) => ({ + ...state, + svomKeywords, + svomKeywordsIsLoading: false, + svomKeywordsIsLoaded: true + })), + on(svomJsonKwActions.loadKwSearchableFail, (state) => ({ + ...state, + svomKeywordsIsLoading: false, + svomKeywordsIsLoaded: false + })), +); + +export const selectAcronymSelected = (state: State) => state.acronymSelected; +export const selectSvomKeywords = (state: State) => state.svomKeywords; +export const selectSvomKeywordsIsLoading = (state: State) => state.svomKeywordsIsLoading; +export const selectSvomKeywordsIsLoaded = (state: State) => state.svomKeywordsIsLoaded; diff --git a/client/src/app/instance/store/selectors/svom-json-kw.selector.spec.ts b/client/src/app/instance/store/selectors/svom-json-kw.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3e9604ca9f040164052454c0725d3990afa4822 --- /dev/null +++ b/client/src/app/instance/store/selectors/svom-json-kw.selector.spec.ts @@ -0,0 +1,24 @@ +import * as svomJsonKwSelector from './svom-json-kw.selector'; +import * as fromSvomJsonKw from '../reducers/svom-json-kw.reducer'; + +describe('[Instance][Store] Svom Json Kw selector', () => { + it('should get selectAcronymSelected', () => { + const state = { instance: { svomJsonKw: { ...fromSvomJsonKw.initialState }}}; + expect(svomJsonKwSelector.selectAcronymSelected(state)).toBeNull(); + }); + + it('should get selectSvomKeywords', () => { + const state = { instance: { svomJsonKw: { ...fromSvomJsonKw.initialState }}}; + expect(svomJsonKwSelector.selectSvomKeywords(state)).toEqual([]); + }); + + it('should get selectSvomKeywordsIsLoading', () => { + const state = { instance: { svomJsonKw: { ...fromSvomJsonKw.initialState }}}; + expect(svomJsonKwSelector.selectSvomKeywordsIsLoading(state)).toBeFalsy(); + }); + + it('should get selectSvomKeywordsIsLoaded', () => { + const state = { instance: { svomJsonKw: { ...fromSvomJsonKw.initialState }}}; + expect(svomJsonKwSelector.selectSvomKeywordsIsLoaded(state)).toBeFalsy(); + }); +}); diff --git a/client/src/app/instance/store/selectors/svom-json-kw.selector.ts b/client/src/app/instance/store/selectors/svom-json-kw.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..c178ec6252e68bd9ef6c5f62d1ab836572cfb970 --- /dev/null +++ b/client/src/app/instance/store/selectors/svom-json-kw.selector.ts @@ -0,0 +1,38 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../../instance.reducer'; +import * as svomJsonKw from '../reducers/svom-json-kw.reducer'; + +export const selectSvomJsonKwState = createSelector( + reducer.getInstanceState, + (state: reducer.State) => state.svomJsonKw +); + +export const selectAcronymSelected = createSelector( + selectSvomJsonKwState, + svomJsonKw.selectAcronymSelected +); + +export const selectSvomKeywords = createSelector( + selectSvomJsonKwState, + svomJsonKw.selectSvomKeywords +); + +export const selectSvomKeywordsIsLoading = createSelector( + selectSvomJsonKwState, + svomJsonKw.selectSvomKeywordsIsLoading +); + +export const selectSvomKeywordsIsLoaded = createSelector( + selectSvomJsonKwState, + svomJsonKw.selectSvomKeywordsIsLoaded +); diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts index cabc079690c1bfea8a44fddb9baa7123796937b2..795315e994b19b011ba88241a4428380e6932c09 100644 --- a/client/src/app/instance/store/services/index.ts +++ b/client/src/app/instance/store/services/index.ts @@ -2,10 +2,12 @@ import { SearchService } from './search.service'; import { SampService } from './samp.service'; import { ConeSearchService } from './cone-search.service'; import { DetailService } from './detail.service'; +import { SvomJsonKwService } from './svom-json-kw.service'; export const instanceServices = [ SearchService, SampService, ConeSearchService, - DetailService + DetailService, + SvomJsonKwService ]; diff --git a/client/src/app/instance/store/services/svom-json-kw.service.spec.ts b/client/src/app/instance/store/services/svom-json-kw.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a334fd2e614672688049927afef84a0273f165e --- /dev/null +++ b/client/src/app/instance/store/services/svom-json-kw.service.spec.ts @@ -0,0 +1,48 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { SvomJsonKwService } from './svom-json-kw.service'; +import { AppConfigService } from 'src/app/app-config.service'; + +describe('[Instance][Store] SvomJsonKwService', () => { + let service: SvomJsonKwService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AppConfigService, useValue: { apiUrl: 'http://testing.com' } }, + SvomJsonKwService + ] + }); + service = TestBed.inject(SvomJsonKwService); + }); + + it('#retrieveData() should return an Observable<any[]>', + inject([HttpTestingController, SvomJsonKwService],(httpMock: HttpTestingController, svomJsonKwService: SvomJsonKwService) => { + const mockResponse = ['myData']; + + svomJsonKwService.loadKwSearchable('OBLC_ECL').subscribe((event: any[]) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('http://testing.com/search/sp_cards?a=8&c=1::eq::OBLC_ECL'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('json'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + } + ) + ); +}); \ No newline at end of file diff --git a/client/src/app/instance/store/services/svom-json-kw.service.ts b/client/src/app/instance/store/services/svom-json-kw.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a990aa7212a49ea99276349219e44b052d3f011 --- /dev/null +++ b/client/src/app/instance/store/services/svom-json-kw.service.ts @@ -0,0 +1,30 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map } from 'rxjs/operators'; + +import { AppConfigService } from 'src/app/app-config.service'; +import { SvomKeyword } from '../models'; + +/** + * @class + * @classdesc Svom Json Kw service. + */ +@Injectable() +export class SvomJsonKwService { + constructor(private http: HttpClient, private config: AppConfigService) { } + + loadKwSearchable(acronym: string) { + return this.http.get<{search_kw: SvomKeyword[]}[]>(`${this.config.apiUrl}/search/sp_cards?a=8&c=1::eq::${acronym}`).pipe( + map(data => data[0].search_kw) + ); + } +} diff --git a/client/src/styles.scss b/client/src/styles.scss index db95f8b3e83d57b52cc97d8b8c0f4bb9a6f0a8b4..dc2941c08270668fa03342dfc3a55ea18eb2eb28 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -44,6 +44,14 @@ main { margin-top: 100px; } +.ng-select-container { + height: 38px !important; +} + +.ng-select.ng-select-disabled>.ng-select-container { + background-color: #e9ecef !important; +} + .custom-switch label, .custom-radio label { cursor: pointer; }