Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Maintenance - Mise à jour mensuelle Lundi 6 Février entre 7h00 et 9h00
Open sidebar
urgi-is
data-discovery
Commits
5e87004e
Commit
5e87004e
authored
Jul 26, 2018
by
Jean-Baptiste Nizet
Browse files
feat: implement suggestions on the frontend
parent
08c4e4ed
Changes
10
Hide whitespace changes
Inline
Side-by-side
frontend/src/app/app.module.ts
View file @
5e87004e
...
...
@@ -10,7 +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
'
;
import
{
NgbPaginationModule
,
NgbTypeaheadModule
}
from
'
@ng-bootstrap/ng-bootstrap
'
;
import
{
registerLocaleData
}
from
'
@angular/common
'
;
import
localeFr
from
'
@angular/common/locales/fr
'
;
...
...
@@ -29,7 +29,8 @@ registerLocaleData(localeFr);
RouterModule
.
forRoot
(
routes
),
ReactiveFormsModule
,
HttpClientModule
,
NgbPaginationModule
.
forRoot
()
NgbPaginationModule
.
forRoot
(),
NgbTypeaheadModule
.
forRoot
()
],
providers
:
[
{
provide
:
LOCALE_ID
,
useValue
:
'
fr-FR
'
}
...
...
frontend/src/app/home/home.component.html
View file @
5e87004e
<div
class=
"mt-5"
>
<form
class=
"input-group"
[formGroup]=
"searchForm"
(ngSubmit)=
"search()"
>
<form
class=
"input-group"
[formGroup]=
"searchForm"
(ngSubmit)=
"search()"
>
<input
class=
"form-control form-control-lg"
type=
"text"
placeholder=
"Exemples : pisum sativum, rosa"
formControlName=
"search"
>
formControlName=
"search"
[ngbTypeahead]=
"suggesterTypeahead"
>
<div
class=
"input-group-append"
>
<button
class=
"btn btn-outline-secondary"
type=
"submit"
>
Recherche
</button>
</div>
...
...
frontend/src/app/home/home.component.spec.ts
View file @
5e87004e
...
...
@@ -5,6 +5,9 @@ import { Router } from '@angular/router';
import
{
ComponentTester
,
speculoosMatchers
}
from
'
ngx-speculoos
'
;
import
{
HomeComponent
}
from
'
./home.component
'
;
import
{
HttpClientTestingModule
}
from
'
@angular/common/http/testing
'
;
import
{
NgbTypeaheadModule
}
from
'
@ng-bootstrap/ng-bootstrap
'
;
import
{
SearchService
}
from
'
../search.service
'
;
class
HomeComponentTester
extends
ComponentTester
<
HomeComponent
>
{
constructor
()
{
...
...
@@ -22,7 +25,7 @@ class HomeComponentTester extends ComponentTester<HomeComponent> {
describe
(
'
HomeComponent
'
,
()
=>
{
beforeEach
(()
=>
TestBed
.
configureTestingModule
({
imports
:
[
ReactiveFormsModule
,
RouterTestingModule
],
imports
:
[
ReactiveFormsModule
,
RouterTestingModule
,
HttpClientTestingModule
,
NgbTypeaheadModule
.
forRoot
()
],
declarations
:
[
HomeComponent
]
}));
...
...
@@ -32,7 +35,10 @@ describe('HomeComponent', () => {
// given a component
const
router
=
TestBed
.
get
(
Router
)
as
Router
;
spyOn
(
router
,
'
navigate
'
);
const
component
=
new
HomeComponent
(
router
);
const
searchService
=
TestBed
.
get
(
SearchService
)
as
SearchService
;
const
component
=
new
HomeComponent
(
router
,
searchService
);
// with a query
const
query
=
'
Bacteria
'
;
...
...
frontend/src/app/home/home.component.ts
View file @
5e87004e
import
{
Component
}
from
'
@angular/core
'
;
import
{
Component
,
OnInit
}
from
'
@angular/core
'
;
import
{
FormControl
,
FormGroup
}
from
'
@angular/forms
'
;
import
{
Router
}
from
'
@angular/router
'
;
import
{
Observable
}
from
'
rxjs
'
;
import
{
SearchService
}
from
'
../search.service
'
;
@
Component
({
selector
:
'
rare-home
'
,
templateUrl
:
'
./home.component.html
'
,
styleUrls
:
[
'
./home.component.scss
'
]
})
export
class
HomeComponent
{
export
class
HomeComponent
implements
OnInit
{
searchForm
:
FormGroup
;
suggesterTypeahead
:
(
text$
:
Observable
<
string
>
)
=>
Observable
<
Array
<
string
>>
;
constructor
(
private
router
:
Router
)
{
constructor
(
private
router
:
Router
,
private
searchService
:
SearchService
)
{
this
.
searchForm
=
new
FormGroup
({
search
:
new
FormControl
()
});
}
ngOnInit
():
void
{
this
.
suggesterTypeahead
=
this
.
searchService
.
getSuggesterTypeahead
();
}
search
()
{
this
.
router
.
navigate
([
'
/search
'
],
{
queryParams
:
{
...
...
@@ -23,5 +31,4 @@ export class HomeComponent {
}
});
}
}
frontend/src/app/search.service.spec.ts
View file @
5e87004e
import
{
TestBed
}
from
'
@angular/core/testing
'
;
import
{
fakeAsync
,
TestBed
,
tick
}
from
'
@angular/core/testing
'
;
import
{
HttpClientTestingModule
,
HttpTestingController
}
from
'
@angular/common/http/testing
'
;
import
{
Subject
}
from
'
rxjs
'
;
import
{
toGeneticResource
,
toSinglePage
}
from
'
./models/test-model-generators
'
;
import
{
SearchService
}
from
'
./search.service
'
;
...
...
@@ -7,12 +8,19 @@ import { Page } from './models/page';
import
{
GeneticResourceModel
}
from
'
./models/genetic-resource.model
'
;
describe
(
'
SearchService
'
,
()
=>
{
beforeEach
(()
=>
TestBed
.
configureTestingModule
({
imports
:
[
HttpClientTestingModule
]
}));
let
service
:
SearchService
;
let
http
:
HttpTestingController
;
beforeEach
(()
=>
{
TestBed
.
configureTestingModule
({
imports
:
[
HttpClientTestingModule
]
});
service
=
TestBed
.
get
(
SearchService
)
as
SearchService
;
http
=
TestBed
.
get
(
HttpTestingController
)
as
HttpTestingController
;
});
it
(
'
should search for the query
'
,
()
=>
{
const
service
=
TestBed
.
get
(
SearchService
)
as
SearchService
;
let
actualResults
:
Page
<
GeneticResourceModel
>
;
service
.
search
(
'
Bacteria
'
,
2
)
.
subscribe
(
results
=>
actualResults
=
results
);
...
...
@@ -20,10 +28,57 @@ describe('SearchService', () => {
const
resource
=
toGeneticResource
(
'
Bacteria
'
);
const
expectedResults
=
toSinglePage
([
resource
]);
const
http
=
TestBed
.
get
(
HttpTestingController
)
as
HttpTestingController
;
http
.
expectOne
(
'
/api/genetic-resources?query=Bacteria&page=1
'
).
flush
(
expectedResults
);
expect
(
actualResults
).
toEqual
(
expectedResults
);
http
.
verify
();
});
it
(
'
should have a typeahead that suggests if text longer than 1, after 300 ms and only if changed
'
,
fakeAsync
(()
=>
{
const
typeahead
=
service
.
getSuggesterTypeahead
();
const
entered
=
new
Subject
<
string
>
();
const
results
:
Array
<
Array
<
string
>>
=
[];
typeahead
(
entered
).
subscribe
(
result
=>
results
.
push
(
result
));
// simulate what is entered, character by character, after a delay between each one
entered
.
next
(
'
'
);
tick
(
100
);
entered
.
next
(
'
v
'
);
tick
(
300
);
// should not trigger a search, but emit an empty array because input is too short
entered
.
next
(
'
vi
'
);
tick
(
100
);
entered
.
next
(
'
vit
'
);
tick
(
100
);
entered
.
next
(
'
vit
'
);
tick
(
100
);
entered
.
next
(
'
vit
'
);
tick
(
300
);
// should finally trigger a search for 'vit'
const
vitResult
=
[
'
vitis
'
,
'
vitis vinifera
'
];
http
.
expectOne
(
'
/api/genetic-resources-suggestions?query=vit
'
).
flush
(
vitResult
);
entered
.
next
(
'
viti
'
);
tick
(
100
);
entered
.
next
(
'
viti
'
);
tick
(
100
);
entered
.
next
(
'
vit
'
);
tick
(
300
);
// should not trigger a second search since same value
entered
.
next
(
'
viti
'
);
tick
(
100
);
entered
.
next
(
'
vitis
'
);
tick
(
100
);
entered
.
next
(
'
vitis
'
);
tick
(
100
);
entered
.
next
(
'
vitis v
'
);
tick
(
300
);
// should trigger a second search
const
vitisVResult
=
[
'
vitis vinifera
'
];
http
.
expectOne
(
'
/api/genetic-resources-suggestions?query=vitis%20v
'
).
flush
(
vitisVResult
);
expect
(
results
).
toEqual
([[],
vitResult
,
vitisVResult
]);
http
.
verify
();
}));
});
frontend/src/app/search.service.ts
View file @
5e87004e
import
{
Injectable
}
from
'
@angular/core
'
;
import
{
HttpClient
}
from
'
@angular/common/http
'
;
import
{
Observable
}
from
'
rxjs
'
;
import
{
Observable
,
of
}
from
'
rxjs
'
;
import
{
catchError
,
debounceTime
,
distinctUntilChanged
,
map
,
switchMap
}
from
'
rxjs/operators
'
;
import
{
GeneticResourceModel
}
from
'
./models/genetic-resource.model
'
;
import
{
Page
}
from
'
./models/page
'
;
...
...
@@ -12,6 +13,9 @@ export class SearchService {
constructor
(
private
http
:
HttpClient
)
{}
/**
* Searches genetic resources for the given query (full-text search), and retrieves the given page (starting at 1)
*/
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
();
...
...
@@ -20,4 +24,35 @@ export class SearchService {
});
}
/**
* Returns what the NgbTypeahead directive, used for search term suggestions, needs as an input
* (see https://ng-bootstrap.github.io/#/components/typeahead/api), i.e. a function that takes an Observable<string>
* as argument, representing the stream of values entered in the search field, and returns an
* Observable<Array<string>>, representing the stream of suggestions to display for each string emitted by the
* observable taken as argument.
*/
getSuggesterTypeahead
():
((
obs
:
Observable
<
string
>
)
=>
Observable
<
Array
<
string
>>
)
{
return
(
text$
:
Observable
<
string
>
)
=>
text$
.
pipe
(
map
(
query
=>
query
.
trim
()),
// start by ignoring the spaces at the beginning or end of the query
debounceTime
(
300
),
// only send a query if the user has stopped writing in the field for at least 300ms.
distinctUntilChanged
(),
// avoid sending a request if the input hasn't changed (for example igf the user enters
// a character then backspace)
switchMap
(
query
=>
{
// get the suggestions for the entered string
if
(
query
.
length
<
2
)
{
// if the query is only one character, prefer suggesting nothing: too vague
return
of
([]);
}
return
this
.
suggest
(
query
).
pipe
(
// otherwise send a request to the server
catchError
(()
=>
of
([]))
// but if the request fails, suggest nothing
);
}
)
);
}
private
suggest
(
query
:
string
):
Observable
<
Array
<
string
>>
{
return
this
.
http
.
get
<
Array
<
string
>>
(
'
/api/genetic-resources-suggestions
'
,
{
params
:
{
query
}
});
}
}
frontend/src/app/search/search.component.html
View file @
5e87004e
...
...
@@ -2,7 +2,8 @@
<form
class=
"input-group"
[formGroup]=
"searchForm"
(ngSubmit)=
"newSearch()"
>
<input
class=
"form-control"
type=
"text"
placeholder=
"Exemples : pisum sativum, rosa"
formControlName=
"search"
>
formControlName=
"search"
[ngbTypeahead]=
"suggesterTypeahead"
>
<div
class=
"input-group-append"
>
<button
class=
"btn btn-outline-secondary"
type=
"submit"
>
Recherche
</button>
</div>
...
...
frontend/src/app/search/search.component.spec.ts
View file @
5e87004e
...
...
@@ -4,7 +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
{
NgbPagination
,
NgbPaginationModule
,
NgbTypeaheadModule
}
from
'
@ng-bootstrap/ng-bootstrap
'
;
import
{
of
}
from
'
rxjs
'
;
import
{
ComponentTester
,
fakeRoute
,
speculoosMatchers
}
from
'
ngx-speculoos
'
;
...
...
@@ -39,7 +39,13 @@ class SearchComponentTester extends ComponentTester<SearchComponent> {
describe
(
'
SearchComponent
'
,
()
=>
{
beforeEach
(()
=>
TestBed
.
configureTestingModule
({
imports
:
[
ReactiveFormsModule
,
RouterTestingModule
,
HttpClientTestingModule
,
NgbPaginationModule
.
forRoot
()],
imports
:
[
ReactiveFormsModule
,
RouterTestingModule
,
HttpClientTestingModule
,
NgbPaginationModule
.
forRoot
(),
NgbTypeaheadModule
.
forRoot
()
],
declarations
:
[
SearchComponent
,
GeneticResourcesComponent
,
GeneticResourceComponent
]
}));
...
...
frontend/src/app/search/search.component.ts
View file @
5e87004e
import
{
Component
,
OnInit
}
from
'
@angular/core
'
;
import
{
ActivatedRoute
,
Router
}
from
'
@angular/router
'
;
import
{
FormControl
,
FormGroup
}
from
'
@angular/forms
'
;
import
{
EMPTY
}
from
'
rxjs
'
;
import
{
EMPTY
,
Observable
}
from
'
rxjs
'
;
import
{
catchError
,
switchMap
}
from
'
rxjs/operators
'
;
import
{
SearchService
}
from
'
../search.service
'
;
...
...
@@ -17,6 +17,7 @@ export class SearchComponent implements OnInit {
query
=
''
;
searchForm
:
FormGroup
;
results
:
Page
<
GeneticResourceModel
>
;
suggesterTypeahead
:
(
text$
:
Observable
<
string
>
)
=>
Observable
<
Array
<
string
>>
;
constructor
(
private
route
:
ActivatedRoute
,
private
router
:
Router
,
private
searchService
:
SearchService
)
{
this
.
searchForm
=
new
FormGroup
({
...
...
@@ -42,6 +43,8 @@ export class SearchComponent implements OnInit {
})
)
.
subscribe
(
results
=>
this
.
results
=
results
);
this
.
suggesterTypeahead
=
this
.
searchService
.
getSuggesterTypeahead
();
}
/**
...
...
frontend/src/custom-bootstrap.scss
View file @
5e87004e
...
...
@@ -13,7 +13,7 @@
@import
"../node_modules/bootstrap/scss/forms"
;
@import
"../node_modules/bootstrap/scss/buttons"
;
// @import "../node_modules/bootstrap/scss/transitions";
//
@import "../node_modules/bootstrap/scss/dropdown";
@import
"../node_modules/bootstrap/scss/dropdown"
;
// @import "../node_modules/bootstrap/scss/button-group";
@import
"../node_modules/bootstrap/scss/input-group"
;
// @import "../node_modules/bootstrap/scss/custom-forms";
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment