Skip to content
Snippets Groups Projects
Commit 0b7fd97f authored by Guillaume Cornut's avatar Guillaume Cornut
Browse files

fix: Fix updating URL and criteria on suggestion field change. Add and fix tests. GNP-5430.

parent e8fc6681
No related branches found
No related tags found
1 merge request!5Implement result page
......@@ -8,7 +8,8 @@
inputId="crops"
criteriaField="crops"
[criteria$]="criteria$"
(selectionChange)="selectionChange.emit($event)"></gpds-suggestion-field>
(selectionChange)="selectionChange.emit($event)">
</gpds-suggestion-field>
</div>
</div>
<div class="form-group row">
......@@ -21,6 +22,8 @@
inputId="germplasmList"
criteriaField="germplasmLists"
[criteria$]="criteria$"
(selectionChange)="selectionChange.emit($event)"></gpds-suggestion-field>
(selectionChange)="selectionChange.emit($event)">
</gpds-suggestion-field>
</div>
</div>
......@@ -17,7 +17,7 @@
</ng-template>
<input type="text"
class="form-control"
id="{{inputId}}"
[id]="inputId"
#instance="ngbTypeahead"
[formControl]="input"
[ngbTypeahead]="search"
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, TestBed } from '@angular/core/testing';
import { SuggestionFieldComponent } from './suggestion-field.component';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { GnpisService } from '../../gnpis.service';
import { of } from 'rxjs';
import { EMPTY_CRITERIA } from '../../model/criteria/dataDiscoveryCriteria';
describe('SuggestionFieldComponent', () => {
let component: SuggestionFieldComponent;
let fixture: ComponentFixture<SuggestionFieldComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SuggestionFieldComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SuggestionFieldComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
const service = jasmine.createSpyObj(
'GnpisService', ['suggest']
);
let component;
let fixture;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
ReactiveFormsModule,
NgbTypeaheadModule
],
declarations: [
SuggestionFieldComponent
],
providers: [
{ provide: GnpisService, useValue: service }
]
});
fixture = TestBed.createComponent(SuggestionFieldComponent);
component = fixture.componentInstance;
component.criteria$ = of(EMPTY_CRITERIA);
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should fetch suggestion', async(() => {
component.criteriaField = 'crops';
const expectedSuggestions = ['a', 'b', 'c'];
service.suggest.and.returnValue(of(expectedSuggestions));
component.search(of('bar'))
.subscribe((actualSuggestions: string[]) => {
expect(actualSuggestions).toEqual(expectedSuggestions);
});
}));
it('should display the selected criteria as pills', () => {
component.criteriaField = 'crops';
component.criteria$ = of({ ...EMPTY_CRITERIA, crops: ['Zea', 'Wheat'] });
fixture.detectChanges();
const pills = fixture.nativeElement.querySelectorAll('.badge-pill');
expect(pills.length).toBe(2);
expect(pills[0].textContent).toContain('Zea');
expect(pills[1].textContent).toContain('Wheat');
});
it('should fetch suggestion', async(() => {
component.criteriaField = 'crops';
const selectedCrops = ['Zea', 'Wheat'];
component.criteria$ = of({ ...EMPTY_CRITERIA, crops: selectedCrops });
const allSuggestions = ['Zea', 'Wheat', 'Vitis', 'Grapevine'];
service.suggest.and.returnValue(of(allSuggestions));
const expectedSuggestions = allSuggestions.filter(s => selectedCrops.indexOf(s) < 0);
fixture.detectChanges();
component.search(of('bar'))
.subscribe((actualSuggestions: string[]) => {
expect(actualSuggestions).toEqual(expectedSuggestions);
});
}));
});
......@@ -6,6 +6,9 @@ import { GnpisService } from '../../gnpis.service';
import { debounceTime, filter, map, switchMap } from 'rxjs/operators';
import { DataDiscoveryCriteria } from '../../model/criteria/dataDiscoveryCriteria';
/**
* Represent field selection along with the name of the field
*/
export interface NamedSelection {
name: string;
selection: string[];
......@@ -33,7 +36,7 @@ export class SuggestionFieldComponent implements OnInit {
@ViewChild('instance') instance: NgbTypeahead;
private lastCriteria: DataDiscoveryCriteria;
private criteria: DataDiscoveryCriteria = null;
private criteriaChanged = true;
constructor(private gnpisService: GnpisService) {
......@@ -41,9 +44,14 @@ export class SuggestionFieldComponent implements OnInit {
ngOnInit(): void {
this.criteria$.subscribe(criteria => {
// When criteria changes
this.criteriaChanged = true;
this.lastCriteria = criteria;
this.selectedKeys = criteria[this.criteriaField];
if (!this.criteria) {
// Criteria first initialized
this.criteria = criteria;
this.selectedKeys = this.criteria[this.criteriaField];
}
});
}
......@@ -52,6 +60,7 @@ export class SuggestionFieldComponent implements OnInit {
* suggestions
*/
search = (text$: Observable<string>): Observable<string[]> => {
// Observable of clicks when the suggestion popup is closed
const clicksWithClosedPopup$ = this.click$.pipe(
filter(() => !this.instance.isPopupOpen())
);
......@@ -59,13 +68,25 @@ export class SuggestionFieldComponent implements OnInit {
let lastSearchTerm: string = null;
let lastSuggestions: string[] = null;
// When new text or focus or click with popup closed
return merge(text$, this.focus$, clicksWithClosedPopup$).pipe(
debounceTime(250),
switchMap((term: string) => {
if (!this.criteriaChanged && lastSearchTerm === term && lastSuggestions) {
// Criteria hasn't changed and text hasn't changed and
// suggestions already fetched
if (
!this.criteriaChanged
&& lastSearchTerm === term
&& lastSuggestions
) {
// Return last suggestions without selected values
return of(this.removeAlreadySelected(lastSuggestions));
}
// Otherwise, we fetch new suggestions
const suggestions$ = this.fetchSuggestion(term);
// Store text and suggestions for re-use
lastSearchTerm = term;
this.criteriaChanged = false;
return suggestions$.pipe(map(suggestions => {
......@@ -76,10 +97,13 @@ export class SuggestionFieldComponent implements OnInit {
);
};
fetchSuggestion(term: string): Observable<string[]> {
/**
* Fetch new suggestions for term
*/
private fetchSuggestion(term: string): Observable<string[]> {
// Fetch suggestions
const suggestions$ = this.gnpisService.suggest(
this.criteriaField, 10, term, this.lastCriteria
this.criteriaField, 10, term, this.criteria
);
// Filter out already selected suggestions
......@@ -122,6 +146,9 @@ export class SuggestionFieldComponent implements OnInit {
this.emitSelectionChange();
}
/**
* Emit current selection when changed
*/
private emitSelectionChange() {
this.selectionChange.emit({
name: this.criteriaField,
......
import { getTestBed, TestBed } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { GnpisService } from './gnpis.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('GnpisService', () => {
let injector;
let service;
let httpMock;
......@@ -13,9 +12,8 @@ describe('GnpisService', () => {
imports: [HttpClientTestingModule],
providers: [HttpClientTestingModule]
});
injector = getTestBed();
service = injector.get(GnpisService);
httpMock = injector.get(HttpTestingController);
service = TestBed.get(GnpisService);
httpMock = TestBed.get(HttpTestingController);
});
it('should be created', () => {
......
......@@ -6,4 +6,4 @@ export interface DataDiscoveryCriteria {
export const EMPTY_CRITERIA: DataDiscoveryCriteria = {
crops: [],
germplasmLists: []
}
};
import { Component, OnInit } from '@angular/core';
import { NamedSelection } from '../form/suggestion-field/suggestion-field.component';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { DataDiscoveryCriteria, EMPTY_CRITERIA } from '../model/criteria/dataDiscoveryCriteria';
import { BehaviorSubject } from 'rxjs';
......@@ -12,36 +12,33 @@ import { BehaviorSubject } from 'rxjs';
})
export class ResultComponent implements OnInit {
criteria: DataDiscoveryCriteria = { ...EMPTY_CRITERIA };
criteria$ = new BehaviorSubject<DataDiscoveryCriteria>(this.criteria);
criteria$ = new BehaviorSubject<DataDiscoveryCriteria>({ ...EMPTY_CRITERIA });
constructor(private route: ActivatedRoute, private router: Router) {
}
onSelectionChanges(namedSelection: NamedSelection) {
this.criteria[namedSelection.name] = namedSelection.selection;
this.criteria$.next(this.criteria);
this.router.navigate(['.'], {
relativeTo: this.route,
queryParams: <Params>this.criteria
queryParams: { [namedSelection.name]: namedSelection.selection },
queryParamsHandling: 'merge'
});
}
ngOnInit(): void {
this.route.queryParams.subscribe(queryParams => {
this.criteria = { ...EMPTY_CRITERIA };
const criteria = this.criteria$.value;
for (const key of Object.keys(queryParams)) {
const value = queryParams[key];
if (Array.isArray(value)) {
// Multiple value query param
this.criteria[key] = value;
criteria[key] = value;
} else {
// Single value query param
this.criteria[key].push(value);
criteria[key] = [value];
}
}
this.criteria$.next(this.criteria);
this.criteria$.next(criteria);
});
}
}
......@@ -114,7 +114,8 @@
"radix": true,
"semicolon": [
true,
"always"
"always",
"ignore-bound-class-methods"
],
"triple-equals": [
true,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment