Commit e188b011 authored by Exbrayat Cédric's avatar Exbrayat Cédric Committed by Jean-Baptiste Nizet
Browse files

feat: add pagination

parent 0e81fdbb
......@@ -20,6 +20,7 @@
"@angular/platform-browser": "6.0.9",
"@angular/platform-browser-dynamic": "6.0.9",
"@angular/router": "6.0.9",
"@ng-bootstrap/ng-bootstrap": "2.2.0",
"bootstrap": "4.1.2",
"core-js": "2.5.7",
"rxjs": "6.2.2",
......
......@@ -10,6 +10,7 @@ import { HomeComponent } from './home/home.component';
import { SearchComponent } from './search/search.component';
import { GeneticResourcesComponent } from './genetic-resources/genetic-resources.component';
import { GeneticResourceComponent } from './genetic-resource/genetic-resource.component';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [
......@@ -23,7 +24,8 @@ import { GeneticResourceComponent } from './genetic-resource/genetic-resource.co
BrowserModule,
RouterModule.forRoot(routes),
ReactiveFormsModule,
HttpClientModule
HttpClientModule,
NgbPaginationModule.forRoot()
],
providers: [],
bootstrap: [AppComponent]
......
......@@ -4,7 +4,7 @@ import { GeneticResourceModel } from './genetic-resource.model';
export function toSinglePage<T>(content: Array<T>): Page<T> {
return {
content,
number: 1,
number: 0,
size: content ? content.length : 0,
totalElements: content ? content.length : 0,
totalPages: 1
......
......@@ -14,14 +14,14 @@ describe('SearchService', () => {
it('should search for the query', () => {
const service = TestBed.get(SearchService) as SearchService;
let actualResults: Page<GeneticResourceModel>;
service.search('Bacteria')
service.search('Bacteria', 2)
.subscribe(results => actualResults = results);
const resource = toGeneticResource('Bacteria');
const expectedResults = toSinglePage([resource]);
const http = TestBed.get(HttpTestingController) as HttpTestingController;
http.expectOne('/api/genetic-resources?query=Bacteria').flush(expectedResults);
http.expectOne('/api/genetic-resources?query=Bacteria&page=1').flush(expectedResults);
expect(actualResults).toEqual(expectedResults);
http.verify();
......
......@@ -12,9 +12,11 @@ export class SearchService {
constructor(private http: HttpClient) {}
search(query: string): Observable<Page<GeneticResourceModel>> {
search(query: string, pageAsNumber: number): Observable<Page<GeneticResourceModel>> {
// we decrease the page as the frontend is 1 based, and the backend 0 based.
const page = (pageAsNumber - 1).toString();
return this.http.get<Page<GeneticResourceModel>>('/api/genetic-resources', {
params: { query }
params: { query, page }
});
}
......
<div class="mt-2">
<form class="input-group"[formGroup]="searchForm" (ngSubmit)="newSearch()">
<form class="input-group" [formGroup]="searchForm" (ngSubmit)="newSearch()">
<input class="form-control" type="text"
placeholder="Exemples: canis lupus, levure fromage"
formControlName="search">
......@@ -8,6 +8,16 @@
</div>
</form>
</div>
<!-- pagination -->
<div class="d-flex justify-content-center mt-5">
<!-- we add 1 to the page because ngb-pagination is 1 based -->
<ngb-pagination *ngIf="results" [page]="results.number + 1" (pageChange)="navigateToPage($event)"
[collectionSize]="results.totalElements"
[pageSize]="results.size"
[maxSize]="20">
</ngb-pagination>
</div>
<!-- results -->
<div class="mt-5">
<rare-genetic-resources [geneticResources]="results"></rare-genetic-resources>
</div>
......@@ -4,6 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { By } from '@angular/platform-browser';
import { NgbPagination, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
import { of } from 'rxjs';
import { ComponentTester, fakeRoute, speculoosMatchers } from 'ngx-speculoos';
......@@ -29,11 +30,15 @@ class SearchComponentTester extends ComponentTester<SearchComponent> {
get results() {
return this.debugElement.query(By.directive(GeneticResourcesComponent));
}
get pagination() {
return this.debugElement.query(By.directive(NgbPagination));
}
}
describe('SearchComponent', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule, HttpClientTestingModule],
imports: [ReactiveFormsModule, RouterTestingModule, HttpClientTestingModule, NgbPaginationModule.forRoot()],
declarations: [SearchComponent, GeneticResourcesComponent, GeneticResourceComponent]
}));
......@@ -59,7 +64,33 @@ describe('SearchComponent', () => {
// then the search should be populated
expect(component.searchForm.get('search').value).toBe(query);
// the search service called
expect(searchService.search).toHaveBeenCalledWith(query);
expect(searchService.search).toHaveBeenCalledWith(query, 0);
// and the results fetched
expect(component.results).toEqual(results);
});
it('should search on init if there is a query and a page', () => {
// given a component
const router = TestBed.get(Router) as Router;
spyOn(router, 'navigate');
const searchService = TestBed.get(SearchService) as SearchService;
const results = toSinglePage([]);
spyOn(searchService, 'search').and.returnValue(of(results));
// with a query on init
const query = 'Bacteria';
const page = 3;
const queryParams = of({ query, page });
const activatedRoute = fakeRoute({ queryParams });
const component = new SearchComponent(activatedRoute, router, searchService);
// when loading
component.ngOnInit();
// then the search should be populated
expect(component.searchForm.get('search').value).toBe(query);
// the search service called
expect(searchService.search).toHaveBeenCalledWith(query, 3);
// and the results fetched
expect(component.results).toEqual(results);
});
......@@ -80,7 +111,27 @@ describe('SearchComponent', () => {
component.newSearch();
// then it should redirect to the search with correct parameters
expect(router.navigate).toHaveBeenCalledWith(['.'], { relativeTo: activatedRoute, queryParams: { query } });
expect(router.navigate).toHaveBeenCalledWith(['.'], { relativeTo: activatedRoute, queryParams: { query, page: 0 } });
});
it('should navigate to next page when pagination is used', () => {
// given a component
const router = TestBed.get(Router) as Router;
spyOn(router, 'navigate');
const searchService = TestBed.get(SearchService) as SearchService;
spyOn(searchService, 'search');
const query = 'Bacteria';
const queryParams = of({ query });
const activatedRoute = fakeRoute({ queryParams });
const component = new SearchComponent(activatedRoute, router, searchService);
component.query = query;
// when navigating
component.navigateToPage(2);
// then it should redirect to the search with correct parameters
expect(component.page).toBe(2);
expect(router.navigate).toHaveBeenCalledWith(['.'], { relativeTo: activatedRoute, queryParams: { query, page: 2 } });
});
it('should display a search bar and trigger a search', () => {
......@@ -121,5 +172,39 @@ describe('SearchComponent', () => {
expect(tester.results).not.toBeNull();
const componentInstance = tester.results.componentInstance as GeneticResourcesComponent;
expect(componentInstance.geneticResources).toEqual(component.results);
// and a pagination with one page
expect(tester.pagination).not.toBeNull();
const paginationComponent = tester.pagination.componentInstance as NgbPagination;
expect(paginationComponent.page).toBe(1);
expect(paginationComponent.pageCount).toBe(1);
});
it('should display paginated results', () => {
// given a component
const tester = new SearchComponentTester();
const component = tester.componentInstance;
// then it should display results even if empty
expect(tester.results).not.toBeNull();
// when it has paginated results
const resource = toGeneticResource('Bacteria');
component.results = toSinglePage([resource]);
// 10 pages of results (10*10)
component.results.totalElements = 100;
component.results.size = 10;
tester.detectChanges();
// then it should display them
expect(tester.results).not.toBeNull();
const componentInstance = tester.results.componentInstance as GeneticResourcesComponent;
expect(componentInstance.geneticResources).toEqual(component.results);
// and a pagination
expect(tester.pagination).not.toBeNull();
const paginationComponent = tester.pagination.componentInstance as NgbPagination;
expect(paginationComponent.page).toBe(1);
expect(paginationComponent.pageCount).toBe(10);
});
});
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormControl, FormGroup } from '@angular/forms';
import { EMPTY } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { SearchService } from '../search.service';
import { GeneticResourceModel } from '../models/genetic-resource.model';
import { Page } from '../models/page';
import { map, switchMap, tap } from 'rxjs/operators';
@Component({
selector: 'rare-search',
......@@ -13,6 +14,8 @@ import { map, switchMap, tap } from 'rxjs/operators';
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
query = '';
page = 0;
searchForm: FormGroup;
results: Page<GeneticResourceModel>;
......@@ -25,20 +28,39 @@ export class SearchComponent implements OnInit {
ngOnInit(): void {
this.route.queryParamMap
.pipe(
map(params => params.get('query')),
tap(query => this.searchForm.get('search').setValue(query)),
switchMap(query => this.searchService.search(query))
// extract query parameters
switchMap(params => {
this.query = params.get('query');
// set the search field
this.searchForm.get('search').setValue(this.query);
if (params.get('page')) {
this.page = +params.get('page');
}
// launch the search
return this.searchService.search(this.query, this.page)
.pipe(catchError(() => EMPTY));
})
)
.subscribe(results => this.results = results);
}
newSearch() {
const query = this.searchForm.get('search').value;
search() {
this.router.navigate(['.'], {
relativeTo: this.route,
queryParams: {
query
query: this.query,
page: this.page
}
});
}
newSearch() {
this.query = this.searchForm.get('search').value;
this.search();
}
navigateToPage(nextPage: number) {
this.page = nextPage;
this.search();
}
}
......@@ -21,7 +21,7 @@
// @import "../node_modules/bootstrap/scss/navbar";
// @import "../node_modules/bootstrap/scss/card";
// @import "../node_modules/bootstrap/scss/breadcrumb";
// @import "../node_modules/bootstrap/scss/pagination";
@import "../node_modules/bootstrap/scss/pagination";
@import "../node_modules/bootstrap/scss/badge";
// @import "../node_modules/bootstrap/scss/jumbotron";
// @import "../node_modules/bootstrap/scss/alert";
......
......@@ -175,6 +175,10 @@
dependencies:
tslib "^1.9.0"
"@ng-bootstrap/ng-bootstrap@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-2.2.0.tgz#acd514e878a1412f39d50eff691095ecc0882bf3"
"@ngtools/webpack@6.0.8":
version "6.0.8"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-6.0.8.tgz#a05bce526aee9da62bb230a95fba83fee99d0bca"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment