Applied feedback and other fixes

This commit is contained in:
Lotte Hofstede
2018-04-13 15:18:35 +02:00
parent c9e7cdcf9a
commit e56ea2aff8
20 changed files with 107 additions and 83 deletions

View File

@@ -80,7 +80,8 @@
"search_dspace": "Search DSpace"
},
"results": {
"head": "Search Results"
"head": "Search Results",
"no-results": "There were no results for this search"
},
"sidebar": {
"close": "Back to results",

View File

@@ -20,7 +20,7 @@
</ng-container>
</ng-container>
<div class="clearfix toggle-more-filters">
<a class="float-left" *ngIf="!(isLastPage() | async)"
<a class="float-left" *ngIf="!(isLastPage$ | async)"
(click)="showMore()">{{"search.filters.filter.show-more"
| translate}}</a>
<a class="float-right" *ngIf="(currentPage | async) > 1"

View File

@@ -18,6 +18,7 @@ import { SearchOptions } from '../../../search-options.model';
import { RouterStub } from '../../../../shared/testing/router-stub';
import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { PageInfo } from '../../../../core/shared/page-info.model';
describe('SearchFacetFilterComponent', () => {
let comp: SearchFacetFilterComponent;
@@ -56,7 +57,7 @@ describe('SearchFacetFilterComponent', () => {
let router;
const page = Observable.of(0);
const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(null, values)));
const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values)));
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
@@ -121,14 +122,14 @@ describe('SearchFacetFilterComponent', () => {
});
describe('when the getAddParams method is called wih a value', () => {
it('should return the selectedValueq list with the new parameter value', () => {
it('should return the selectedValue list with the new parameter value', () => {
const result = comp.getAddParams(value3);
expect(result).toEqual({ [mockFilterConfig.paramName]: [value1, value2, value3] });
});
});
describe('when the getRemoveParams method is called wih a value', () => {
it('should return the selectedValueq list with the parameter value left out', () => {
it('should return the selectedValue list with the parameter value left out', () => {
const result = comp.getRemoveParams(value1);
expect(result).toEqual({ [mockFilterConfig.paramName]: [value2] });
});

View File

@@ -4,7 +4,7 @@ import { SearchFilterConfig } from '../../../search-service/search-filter-config
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { SearchFilterService } from '../search-filter.service';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { hasNoValue, hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { SearchService } from '../../../search-service/search.service';
@@ -30,6 +30,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
filterValues: Array<Observable<RemoteData<PaginatedList<FacetValue>>>> = [];
filterValues$: BehaviorSubject<any> = new BehaviorSubject(this.filterValues);
currentPage: Observable<number>;
isLastPage$: BehaviorSubject<boolean> = new BehaviorSubject(false);
filter: string;
pageChange = false;
sub: Subscription;
@@ -50,12 +51,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.pageChange = false;
this.unsubscribe();
this.sub = this.currentPage.distinctUntilChanged().map((page) => {
return this.searchService.getFacetValuesFor(this.filterConfig, page, options);
}).subscribe((newValues$) => {
this.filterValues = [...this.filterValues, newValues$];
this.filterValues$.next(this.filterValues);
newValues$.first().subscribe((rd) => this.isLastPage$.next(hasNoValue(rd.payload.next)));
});
}
@@ -98,18 +99,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
hasValue(o: any): boolean {
return hasValue(o);
}
isLastPage(): Observable<boolean> {
return this.filterValues$.flatMap((map) => {
if (isNotEmpty(map)) {
return map.pop().map((rd: RemoteData<PaginatedList<FacetValue>>) => rd.payload.currentPage >= rd.payload.totalPages);
} else {
return false;
}
});
}
getRemoveParams(value: string) {
return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) };
}
@@ -123,7 +112,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
}
unsubscribe(): void {
if (this.sub !== undefined) {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}

View File

@@ -37,5 +37,3 @@
</div>
</div>
</div>

View File

@@ -65,6 +65,8 @@ export class SearchPageComponent implements OnInit {
this.resultsRD$ = this.searchOptions$.pipe(
flatMap((searchOptions) => this.service.search(searchOptions))
);
this.resultsRD$.subscribe((t) => console.log(t));
}
public closeSidebar(): void {

View File

@@ -1,10 +1,11 @@
<h2>{{ 'search.results.head' | translate }}</h2>
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
<ds-viewable-collection
[config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort"
[objects]="searchResults"
[hideGear]="true">
</ds-viewable-collection></div>
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-loading *ngIf="!searchResults || searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
<ds-error *ngIf="searchResults?.payload?.page.length == 0" message="{{'search.results.no-results' | translate}}"></ds-error>

View File

@@ -5,6 +5,7 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { SearchOptions, ViewMode } from '../search-options.model';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { SearchResult } from '../search-result.model';
import { PaginatedList } from '../../core/data/paginated-list';
/**
* This component renders a simple item page.
@@ -20,7 +21,7 @@ import { SearchResult } from '../search-result.model';
]
})
export class SearchResultsComponent {
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
@Input() searchResults: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>;
@Input() searchConfig: SearchOptions;
@Input() sortConfig: SortOptions;
@Input() viewMode: ViewMode;

View File

@@ -10,7 +10,7 @@ import { ViewMode } from '../../+search-page/search-options.model';
import { RouteService } from '../../shared/route.service';
import { GLOBAL_CONFIG } from '../../../config';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { RequestService } from '../../core/data/request.service';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
@@ -79,7 +79,8 @@ describe('SearchService', () => {
const halService = {
/* tslint:disable:no-empty */
getEndpoint: () => {}
getEndpoint: () => {
}
/* tslint:enable:no-empty */
};
@@ -118,6 +119,8 @@ describe('SearchService', () => {
],
});
searchService = TestBed.get(SearchService);
const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } });
router.parseUrl.and.returnValue(urlTree);
});
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
@@ -160,7 +163,8 @@ describe('SearchService', () => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
/* tslint:disable:no-empty */
searchService.search(searchOptions).subscribe((t) => {}); // subscribe to make sure all methods are called
searchService.search(searchOptions).subscribe((t) => {
}); // subscribe to make sure all methods are called
/* tslint:enable:no-empty */
});
@@ -189,7 +193,8 @@ describe('SearchService', () => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
/* tslint:disable:no-empty */
searchService.getConfig(null).subscribe((t) => {}); // subscribe to make sure all methods are called
searchService.getConfig(null).subscribe((t) => {
}); // subscribe to make sure all methods are called
/* tslint:enable:no-empty */
});
@@ -220,7 +225,8 @@ describe('SearchService', () => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
/* tslint:disable:no-empty */
searchService.getConfig(scope).subscribe((t) => {}); // subscribe to make sure all methods are called
searchService.getConfig(scope).subscribe((t) => {
}); // subscribe to make sure all methods are called
/* tslint:enable:no-empty */
});

View File

@@ -1,5 +1,8 @@
import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import {
ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router,
UrlSegmentGroup
} from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { flatMap, map, tap } from 'rxjs/operators';
import { ViewMode } from '../../+search-page/search-options.model';
@@ -21,13 +24,12 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { NormalizedSearchResult } from '../normalized-search-result.model';
import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model';
import { FacetValue } from './facet-value.model';
import { FilterType } from './filter-type.model';
import { SearchFilterConfig } from './search-filter-config.model';
import { SearchResponseParsingService } from '../../core/data/search-response-parsing.service';
import { SearchQueryResponse } from './search-query-response.model';
@@ -37,48 +39,16 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-o
import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service';
import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { observable } from 'rxjs/symbol/observable';
@Injectable()
export class SearchService implements OnDestroy {
private searchLinkPath = 'discover/search/objects';
private facetValueLinkPath = 'discover/search/facets';
private facetValueLinkPathPrefix = 'discover/facets/';
private facetConfigLinkPath = 'discover/facets';
private sub;
uiSearchRoute = '/search';
config: SearchFilterConfig[] = [
// Object.assign(new SearchFilterConfig(),
// {
// name: 'scope',
// type: FilterType.hierarchical,
// hasFacets: true,
// isOpenByDefault: true
// }),
Object.assign(new SearchFilterConfig(),
{
name: 'author',
type: FilterType.text,
hasFacets: true,
isOpenByDefault: false
}),
Object.assign(new SearchFilterConfig(),
{
name: 'dateIssued',
type: FilterType.date,
hasFacets: true,
isOpenByDefault: false
}),
Object.assign(new SearchFilterConfig(),
{
name: 'subject',
type: FilterType.text,
hasFacets: false,
isOpenByDefault: false
})
];
// searchOptions: BehaviorSubject<SearchOptions>;
searchOptions: SearchOptions;
constructor(private router: Router,
@@ -96,7 +66,6 @@ export class SearchService implements OnDestroy {
}
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
// this.halService.getEndpoint(this.searchLinkPath).subscribe((t) => console.log(t));
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url: string) => {
if (hasValue(searchOptions)) {
@@ -136,7 +105,8 @@ export class SearchService implements OnDestroy {
);
// Create search results again with the correct dso objects linked to each result
const tDomainListObs: Observable<Array<SearchResult<DSpaceObject>>> = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData<DSpaceObject[]>) => {
const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData<DSpaceObject[]>) => {
return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
let co = DSpaceObject;
if (dsos.payload[index]) {
@@ -262,11 +232,13 @@ export class SearchService implements OnDestroy {
queryParamsHandling: 'merge'
};
this.router.navigate([this.uiSearchRoute], navigationExtras);
this.router.navigate([this.getSearchLink()], navigationExtras);
}
getSearchLink() {
return this.uiSearchRoute;
const urlTree = this.router.parseUrl(this.router.url);
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
return '/' + g.toString();
}
ngOnDestroy(): void {

View File

@@ -4,7 +4,7 @@ import { map, tap } from 'rxjs/operators';
import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model';
import { SearchResult } from '../../../+search-page/search-result.model';
import { SearchQueryResponse } from '../../../+search-page/search-service/search-query-response.model';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error';
@@ -200,6 +200,11 @@ export class RemoteDataBuildService {
}
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
if (isEmpty(input)) {
return Observable.of(new RemoteData(false, false, true, null, []));
}
return Observable.combineLatest(
...input,
(...arr: Array<RemoteData<T>>) => {

View File

@@ -117,8 +117,9 @@ export abstract class BaseResponseParsingService {
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
}
processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
processPageInfo(payload: any): PageInfo {
if (isNotEmpty(payload.page)) {
const pageObj = Object.assign({}, payload.page, {_links: payload._links});
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
if (pageInfoObject.currentPage >= 0) {
Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 });

View File

@@ -29,7 +29,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(

View File

@@ -28,7 +28,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href);
const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload))
}
}

View File

@@ -37,7 +37,7 @@ export class FacetValueMapResponseParsingService extends BaseResponseParsingServ
payload._embedded.facets.map((facet) => {
const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
const facetValues = serializer.deserializeArray(values);
const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page));
const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
facetMap[facet.name] = valuesResponse;
});

View File

@@ -30,9 +30,9 @@ export class FacetValueResponseParsingService extends BaseResponseParsingService
const payload = data.payload;
const serializer = new DSpaceRESTv2Serializer(FacetValue);
const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
// const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
const facetValues = serializer.deserializeArray(values);
return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page));
const facetValues = serializer.deserializeArray(payload._embedded.values);
return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
}
}

View File

@@ -45,10 +45,44 @@ export class PaginatedList<T> {
return this.pageInfo.currentPage;
}
return 1;
}
set currentPage(value: number) {
this.pageInfo.currentPage = value;
}
get first(): string {
return this.pageInfo.first;
}
set first(first: string) {
this.pageInfo.first = first;
}
get prev(): string {
return this.pageInfo.prev;
}
set prev(prev: string) {
this.pageInfo.prev = prev;
}
get next(): string {
return this.pageInfo.next;
}
set next(next: string) {
this.pageInfo.next = next;
}
get last(): string {
return this.pageInfo.last;
}
set last(last: string) {
this.pageInfo.last = last;
}
}

View File

@@ -56,6 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService {
}));
payload.objects = objects;
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
}
}

View File

@@ -28,4 +28,15 @@ export class PageInfo {
@autoserializeAs(Number, 'number')
currentPage: number;
@autoserialize
last: string;
@autoserialize
next: string;
@autoserialize
prev: string;
@autoserialize
first: string;
}

View File

@@ -1,7 +1,9 @@
export class RouterStub {
url: string;
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
parseUrl = jasmine.createSpy('parseUrl');
navigateByUrl(url): void {
this.url = url;
}