From 4c2cbc55e0273488b7dab6dc8ff6a46449fb8a72 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 13 Feb 2018 10:13:02 +0100 Subject: [PATCH 01/34] intermediate commit --- .../normalized-search-result.model.ts | 13 ++ .../search-facet-filter.component.spec.ts | 4 +- .../search-filter.service.spec.ts | 4 +- .../search-filter/search-filter.service.ts | 2 +- .../search-query-response.model.ts | 47 +++++++ .../search-service/search.service.ts | 133 ++++++------------ src/app/core/browse/browse.service.spec.ts | 38 ++--- src/app/core/browse/browse.service.ts | 10 +- src/app/core/cache/response-cache.models.ts | 17 ++- src/app/core/config/config.service.spec.ts | 2 +- src/app/core/config/config.service.ts | 2 +- .../submission-definitions-config.service.ts | 2 +- .../config/submission-forms-config.service.ts | 2 +- .../submission-sections-config.service.ts | 2 +- src/app/core/core.module.ts | 8 +- src/app/core/data/collection-data.service.ts | 2 +- src/app/core/data/comcol-data.service.spec.ts | 2 +- src/app/core/data/comcol-data.service.ts | 4 +- src/app/core/data/community-data.service.ts | 2 +- src/app/core/data/data.service.ts | 2 +- .../data/debug-response-parsing.service.ts | 13 ++ ... endpoint-map-response-parsing.service.ts} | 7 +- src/app/core/data/item-data.service.ts | 4 +- src/app/core/data/request.models.ts | 22 ++- .../data/search-response-parsing.service.ts | 42 ++++++ .../core/shared/hal-endpoint.service.spec.ts | 30 ++-- src/app/core/shared/hal-endpoint.service.ts | 53 +++++-- 27 files changed, 291 insertions(+), 178 deletions(-) create mode 100644 src/app/+search-page/normalized-search-result.model.ts create mode 100644 src/app/+search-page/search-service/search-query-response.model.ts create mode 100644 src/app/core/data/debug-response-parsing.service.ts rename src/app/core/data/{root-response-parsing.service.ts => endpoint-map-response-parsing.service.ts} (76%) create mode 100644 src/app/core/data/search-response-parsing.service.ts diff --git a/src/app/+search-page/normalized-search-result.model.ts b/src/app/+search-page/normalized-search-result.model.ts new file mode 100644 index 0000000000..2e9a4a8b8e --- /dev/null +++ b/src/app/+search-page/normalized-search-result.model.ts @@ -0,0 +1,13 @@ +import { autoserialize } from 'cerialize'; +import { Metadatum } from '../core/shared/metadatum.model'; +import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; + +export class NormalizedSearchResult implements ListableObject { + + @autoserialize + dspaceObject: string; + + @autoserialize + hitHighlights: Metadatum[]; + +} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index bf069eee60..d6c6da3051 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -96,8 +96,8 @@ describe('SearchFacetFilterComponent', () => { link = comp.getSearchLink(); }); - it('should return the value of the searchLink variable in the filter service', () => { - expect(link).toEqual(filterService.searchLink); + it('should return the value of the uiSearchRoute variable in the filter service', () => { + expect(link).toEqual(filterService.uiSearchRoute); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 7371e55ee8..6dcf8d73b8 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -192,13 +192,13 @@ describe('SearchFilterService', () => { }); }); - describe('when the searchLink method is called', () => { + describe('when the uiSearchRoute method is called', () => { let link: string; beforeEach(() => { link = service.searchLink; }); - it('should return the value of searchLink in the search service', () => { + it('should return the value of uiSearchRoute in the search service', () => { expect(link).toEqual(searchServiceStub.searchLink); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index f3efc19b86..8a909b6fa7 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -46,7 +46,7 @@ export class SearchFilterService { } get searchLink() { - return this.searchService.searchLink; + return this.searchService.uiSearchRoute; } isCollapsed(filterName: string): Observable { diff --git a/src/app/+search-page/search-service/search-query-response.model.ts b/src/app/+search-page/search-service/search-query-response.model.ts new file mode 100644 index 0000000000..b8948d963f --- /dev/null +++ b/src/app/+search-page/search-service/search-query-response.model.ts @@ -0,0 +1,47 @@ +import { autoserialize, autoserializeAs } from 'cerialize'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { NormalizedSearchResult } from '../normalized-search-result.model'; + +export class SearchQueryResponse { + @autoserialize + scope: string; + + @autoserialize + query: string; + + @autoserialize + appliedFilters: any[]; // TODO + + @autoserialize + sort: any; // TODO + + @autoserialize + configurationName: string; + + @autoserialize + public type: string; + + @autoserialize + page: PageInfo; + + @autoserializeAs(NormalizedSearchResult) + objects: NormalizedSearchResult[]; + + @autoserialize + facets: any; // TODO + + @autoserialize + self: string; + + @autoserialize + next: string; + + @autoserialize + previous: string; + + @autoserialize + first: string; + + @autoserialize + last: string; +} diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index c70fe22ce0..a0af63c28d 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,12 +1,25 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { Inject, Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { RestResponse } from '../../core/cache/response-cache.models'; +import { ResponseCacheService } from '../../core/cache/response-cache.service'; +import { DebugResponseParsingService } from '../../core/data/debug-response-parsing.service'; +import { DSOResponseParsingService } from '../../core/data/dso-response-parsing.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; +import { ResponseParsingService } from '../../core/data/parsing.service'; import { RemoteData } from '../../core/data/remote-data'; +import { GetRequest, EndpointMapRequest, RestRequest } from '../../core/data/request.models'; +import { RequestService } from '../../core/data/request.service'; +import { DSpaceRESTV2Response } from '../../core/dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Item } from '../../core/shared/item.model'; import { Metadatum } from '../../core/shared/metadatum.model'; import { PageInfo } from '../../core/shared/page-info.model'; @@ -19,6 +32,7 @@ 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'; function shuffle(array: any[]) { let i = 0; @@ -35,23 +49,11 @@ function shuffle(array: any[]) { } @Injectable() -export class SearchService implements OnDestroy { +export class SearchService extends HALEndpointService implements OnDestroy { + protected linkPath = 'discover/search/objects'; - totalPages = 5; - mockedHighlights: string[] = new Array( - 'This is a sample abstract.', - 'This is a sample abstract. But, to fill up some space, here\'s "Hello" in several different languages : ', - 'This is a Sample HTML webpage including several images and styles (CSS).', - 'This is really just a sample abstract. But, Í’vé thrown ïn a cõuple of spëciâl charactèrs för êxtrå fuñ!', - 'This abstract is really quite great', - 'The solution structure of the bee venom neurotoxin', - 'BACKGROUND: The Open Archive Initiative (OAI) refers to a movement started around the \'90 s to guarantee free access to scientific information', - 'The collision fault detection of a XXY stage is proposed for the first time in this paper', - 'This was blank in the actual item, no abstract', - 'The QSAR DataBank (QsarDB) repository', - ); private sub; - searchLink = '/search'; + uiSearchRoute = '/search'; config: SearchFilterConfig[] = [ Object.assign(new SearchFilterConfig(), @@ -86,11 +88,16 @@ export class SearchService implements OnDestroy { // searchOptions: BehaviorSubject; searchOptions: SearchOptions; - constructor(private itemDataService: ItemDataService, - private routeService: RouteService, - private route: ActivatedRoute, - private router: Router) { - + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + private itemDataService: ItemDataService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private routeService: RouteService, + private route: ActivatedRoute, + private router: Router + ) { + super(); const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; @@ -101,74 +108,18 @@ export class SearchService implements OnDestroy { } search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable>>> { - this.searchOptions = searchOptions; - let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`; - if (hasValue(scopeId)) { - self += `&scope=${scopeId}`; - } - if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.currentPage)) { - self += `&page=${searchOptions.pagination.currentPage}`; - } - if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.pageSize)) { - self += `&pageSize=${searchOptions.pagination.pageSize}`; - } - if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.direction)) { - self += `&sortDirection=${searchOptions.sort.direction}`; - } - if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.field)) { - self += `&sortField=${searchOptions.sort.field}`; - } - - const error = undefined; - const returningPageInfo = new PageInfo(); - - if (isNotEmpty(searchOptions)) { - returningPageInfo.elementsPerPage = searchOptions.pagination.pageSize; - returningPageInfo.currentPage = searchOptions.pagination.currentPage; - } else { - returningPageInfo.elementsPerPage = 10; - returningPageInfo.currentPage = 1; - } - - const itemsObs = this.itemDataService.findAll({ - scopeID: scopeId, - currentPage: returningPageInfo.currentPage, - elementsPerPage: returningPageInfo.elementsPerPage - }); - - return itemsObs - .filter((rd: RemoteData>) => rd.hasSucceeded) - .map((rd: RemoteData>) => { - - const totalElements = rd.payload.totalElements > 20 ? 20 : rd.payload.totalElements; - - const page = shuffle(rd.payload.page) - .map((item: Item, index: number) => { - const mockResult: SearchResult = new ItemSearchResult(); - mockResult.dspaceObject = item; - const highlight = new Metadatum(); - highlight.key = 'dc.description.abstract'; - highlight.value = this.mockedHighlights[index % this.mockedHighlights.length]; - mockResult.hitHighlights = new Array(highlight); - return mockResult; - }); - - const payload = Object.assign({}, rd.payload, { totalElements: totalElements, page }); - - return new RemoteData( - rd.isRequestPending, - rd.isResponsePending, - rd.hasSucceeded, - error, - payload - ) - }).startWith(new RemoteData( - true, - false, - undefined, - undefined, - undefined - )); + const searchEndpointUrlObs = this.getEndpoint(); + searchEndpointUrlObs.pipe( + map((url: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return SearchResponseParsingService; + } + }); + }) + ).subscribe((request: RestRequest) => this.requestService.configure(request)); + return Observable.of(undefined); } getConfig(): Observable> { @@ -231,7 +182,7 @@ export class SearchService implements OnDestroy { queryParamsHandling: 'merge' }; - this.router.navigate([this.searchLink], navigationExtras); + this.router.navigate([this.uiSearchRoute], navigationExtras); } getClearFiltersQueryParams(): any { @@ -249,7 +200,7 @@ export class SearchService implements OnDestroy { } getSearchLink() { - return this.searchLink; + return this.uiSearchRoute; } ngOnDestroy(): void { diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 764107837b..2385948b2f 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -110,12 +110,12 @@ describe('BrowseService', () => { .returnValue(hot('--a-', { a: browsesEndpointURL })); }); - it('should return the URL for the given metadatumKey and linkName', () => { + it('should return the URL for the given metadatumKey and linkPath', () => { const metadatumKey = 'dc.date.issued'; - const linkName = 'items'; - const expectedURL = browseDefinitions[0]._links[linkName]; + const linkPath = 'items'; + const expectedURL = browseDefinitions[0]._links[linkPath]; - const result = service.getBrowseURLFor(metadatumKey, linkName); + const result = service.getBrowseURLFor(metadatumKey, linkPath); const expected = cold('c-d-', { c: undefined, d: expectedURL }); expect(result).toBeObservable(expected); @@ -123,10 +123,10 @@ describe('BrowseService', () => { it('should work when the definition uses a wildcard in the metadatumKey', () => { const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition - const linkName = 'items'; - const expectedURL = browseDefinitions[1]._links[linkName]; + const linkPath = 'items'; + const expectedURL = browseDefinitions[1]._links[linkPath]; - const result = service.getBrowseURLFor(metadatumKey, linkName); + const result = service.getBrowseURLFor(metadatumKey, linkPath); const expected = cold('c-d-', { c: undefined, d: expectedURL }); expect(result).toBeObservable(expected); @@ -134,30 +134,30 @@ describe('BrowseService', () => { it('should throw an error when the key doesn\'t match', () => { const metadatumKey = 'dc.title'; // isn't in the definitions - const linkName = 'items'; + const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkName); - const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadatumKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); it('should throw an error when the link doesn\'t match', () => { const metadatumKey = 'dc.date.issued'; - const linkName = 'collections'; // isn't in the definitions + const linkPath = 'collections'; // isn't in the definitions - const result = service.getBrowseURLFor(metadatumKey, linkName); - const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadatumKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); it('should configure a new BrowseEndpointRequest', () => { const metadatumKey = 'dc.date.issued'; - const linkName = 'items'; + const linkPath = 'items'; const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL); - scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); + scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); @@ -175,9 +175,9 @@ describe('BrowseService', () => { .returnValue(hot('----')); const metadatumKey = 'dc.date.issued'; - const linkName = 'items'; + const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkName); + const result = service.getBrowseURLFor(metadatumKey, linkPath); const expected = cold('b---', { b: undefined }); expect(result).toBeObservable(expected); }); @@ -192,9 +192,9 @@ describe('BrowseService', () => { .returnValue(hot('--a-', { a: browsesEndpointURL })); const metadatumKey = 'dc.date.issued'; - const linkName = 'items'; + const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkName); + const result = service.getBrowseURLFor(metadatumKey, linkPath); const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`)); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index a321e14706..3b283e2d93 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -13,7 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class BrowseService extends HALEndpointService { - protected linkName = 'browses'; + protected linkPath = 'browses'; private static toSearchKeyArray(metadatumKey: string): string[] { const keyParts = metadatumKey.split('.'); @@ -35,7 +35,7 @@ export class BrowseService extends HALEndpointService { super(); } - getBrowseURLFor(metadatumKey: string, linkName: string): Observable { + getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); return this.getEndpoint() .filter((href: string) => isNotEmpty(href)) @@ -59,10 +59,10 @@ export class BrowseService extends HALEndpointService { return isNotEmpty(matchingKeys); }) ).map((def: BrowseDefinition) => { - if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) { - throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`); + if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { + throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`); } else { - return def._links[linkName]; + return def._links[linkPath]; } }) ); diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 06fc26aa67..fb9f10ce51 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -1,3 +1,4 @@ +import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; import { BrowseDefinition } from '../shared/browse-definition.model'; @@ -21,11 +22,21 @@ export class DSOSuccessResponse extends RestResponse { } } -export class EndpointMap { - [linkName: string]: string +export class SearchSuccessResponse extends RestResponse { + constructor( + public results: SearchQueryResponse, + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } } -export class RootSuccessResponse extends RestResponse { +export class EndpointMap { + [linkPath: string]: string +} + +export class EndpointMapSuccessResponse extends RestResponse { constructor( public endpointMap: EndpointMap, public statusCode: string, diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index b0c364a86e..3cdb22948f 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -11,7 +11,7 @@ const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; class TestService extends ConfigService { - protected linkName = LINK_NAME; + protected linkPath = LINK_NAME; protected browseEndpoint = BROWSE; constructor( diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 9ad4684300..a8c2bc46bf 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -16,7 +16,7 @@ export abstract class ConfigService extends HALEndpointService { protected request: ConfigRequest; protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; protected abstract browseEndpoint: string; diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts index 4857569236..9655576e71 100644 --- a/src/app/core/config/submission-definitions-config.service.ts +++ b/src/app/core/config/submission-definitions-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionDefinitionsConfigService extends ConfigService { - protected linkName = 'submissiondefinitions'; + protected linkPath = 'submissiondefinitions'; protected browseEndpoint = 'search/findByCollection'; constructor( diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index 5e992146ee..7209cd0fdf 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionFormsConfigService extends ConfigService { - protected linkName = 'submissionforms'; + protected linkPath = 'submissionforms'; protected browseEndpoint = ''; constructor( diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts index 96a8557e9c..108fc30259 100644 --- a/src/app/core/config/submission-sections-config.service.ts +++ b/src/app/core/config/submission-sections-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionSectionsConfigService extends ConfigService { - protected linkName = 'submissionsections'; + protected linkPath = 'submissionsections'; protected browseEndpoint = ''; constructor( diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 768f05f24b..2960918ea7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -17,7 +17,9 @@ import { isNotEmpty } from '../shared/empty.util'; import { ApiService } from '../shared/api.service'; import { CollectionDataService } from './data/collection-data.service'; import { CommunityDataService } from './data/community-data.service'; +import { DebugResponseParsingService } from './data/debug-response-parsing.service'; import { DSOResponseParsingService } from './data/dso-response-parsing.service'; +import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { HostWindowService } from '../shared/host-window.service'; import { ItemDataService } from './data/item-data.service'; @@ -27,7 +29,7 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RequestService } from './data/request.service'; import { ResponseCacheService } from './cache/response-cache.service'; -import { RootResponseParsingService } from './data/root-response-parsing.service'; +import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; import { ServerResponseService } from '../shared/server-response.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; import { BrowseService } from './browse/browse.service'; @@ -67,7 +69,9 @@ const PROVIDERS = [ RemoteDataBuildService, RequestService, ResponseCacheService, - RootResponseParsingService, + EndpointMapResponseParsingService, + DebugResponseParsingService, + SearchResponseParsingService, ServerResponseService, BrowseResponseParsingService, BrowseService, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index f9f581128b..a0f246aedc 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -13,7 +13,7 @@ import { RequestService } from './request.service'; @Injectable() export class CollectionDataService extends ComColDataService { - protected linkName = 'collections'; + protected linkPath = 'collections'; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index fefe7d3730..0476a901b4 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -22,7 +22,7 @@ class NormalizedTestObject implements CacheableObject { } class TestService extends ComColDataService { - protected linkName = LINK_NAME; + protected linkPath = LINK_NAME; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 68981121c1..cc9ef4ad60 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -17,7 +17,7 @@ export abstract class ComColDataService this.objectCache.getByUUID(scopeID, NormalizedCommunity)) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .map((nc: NormalizedCommunity) => nc._links[this.linkPath]) .filter((href) => isNotEmpty(href)) ).distinctUntilChanged(); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index bbee96ab47..dce67e936f 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -13,7 +13,7 @@ import { RequestService } from './request.service'; @Injectable() export class CommunityDataService extends ComColDataService { - protected linkName = 'communities'; + protected linkPath = 'communities'; protected cds = this; constructor( diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 2d003d6fd1..ccdbbea5b9 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -19,7 +19,7 @@ export abstract class DataService protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; constructor( diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts new file mode 100644 index 0000000000..d530948559 --- /dev/null +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { RestResponse } from '../cache/response-cache.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; + +@Injectable() +export class DebugResponseParsingService implements ResponseParsingService { + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + console.log('request', request, 'data', data); + return undefined; + } +} diff --git a/src/app/core/data/root-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts similarity index 76% rename from src/app/core/data/root-response-parsing.service.ts rename to src/app/core/data/endpoint-map-response-parsing.service.ts index a3e7fc22a3..b850e13932 100644 --- a/src/app/core/data/root-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { ErrorResponse, RestResponse, RootSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { isNotEmpty } from '../../shared/empty.util'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; @Injectable() -export class RootResponseParsingService implements ResponseParsingService { +export class EndpointMapResponseParsingService implements ResponseParsingService { constructor( @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, ) { @@ -21,7 +20,7 @@ export class RootResponseParsingService implements ResponseParsingService { for (const link of Object.keys(links)) { links[link] = links[link].href; } - return new RootSuccessResponse(links, data.statusCode); + return new EndpointMapSuccessResponse(links, data.statusCode); } else { return new ErrorResponse( Object.assign( diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 7e978e0879..155ba357c6 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -17,7 +17,7 @@ import { RequestService } from './request.service'; @Injectable() export class ItemDataService extends DataService { - protected linkName = 'items'; + protected linkPath = 'items'; constructor( protected responseCache: ResponseCacheService, @@ -34,7 +34,7 @@ export class ItemDataService extends DataService { if (isEmpty(scopeID)) { return this.getEndpoint(); } else { - return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) + return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath) .filter((href: string) => isNotEmpty(href)) .map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()) .distinctUntilChanged(); diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ee37f9c3d4..e9d200e15d 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -4,7 +4,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; -import { RootResponseParsingService } from './root-response-parsing.service'; +import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service'; @@ -140,14 +140,24 @@ export class FindAllRequest extends GetRequest { } } -export class RootEndpointRequest extends GetRequest { - constructor(uuid: string, EnvConfig: GlobalConfig) { - const href = new RESTURLCombiner(EnvConfig, '/').toString(); - super(uuid, href); +export class EndpointMapRequest extends GetRequest { + constructor( + public uuid: string, + public href: string, + public body?: any + ) { + super(uuid, href, body); } getResponseParser(): GenericConstructor { - return RootResponseParsingService; + return EndpointMapResponseParsingService; + } +} + +export class RootEndpointRequest extends EndpointMapRequest { + constructor(uuid: string, EnvConfig: GlobalConfig) { + const href = new RESTURLCombiner(EnvConfig, '/').toString(); + super(uuid, href); } } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts new file mode 100644 index 0000000000..bd64101397 --- /dev/null +++ b/src/app/core/data/search-response-parsing.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { + DSOSuccessResponse, RestResponse, + SearchSuccessResponse +} from '../cache/response-cache.models'; +import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { PageInfo } from '../shared/page-info.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; + +@Injectable() +export class SearchResponseParsingService implements ResponseParsingService { + constructor(private dsoParser: DSOResponseParsingService) {} + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + const dsoSelfLinks = payload._embedded.objects + .map((object) => object._embedded.dspaceObject) + // we don't need embedded collections, bitstreamformats, etc for search results. + // And parsing them all takes up a lot of time. Throw them away to improve performance + // until objs until partial results are supported by the rest api + .map((dso) => Object.assign({}, dso, { _embedded: undefined })) + .map((dso) => this.dsoParser.parse(request, { + payload: dso, + statusCode: data.statusCode + })) + .map((obj) => obj.resourceSelfLinks) + .reduce((combined, thisElement) => [...combined, ...thisElement], []); + + const objects = payload._embedded.objects + .map((object, index) => Object.assign({}, object, { dspaceObject: dsoSelfLinks[index] })); + + payload.objects = objects; + const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); + return new SearchSuccessResponse(deserialized, data.statusCode, undefined); + } + +} diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index a47bfd745c..39652d6e7c 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -18,7 +18,7 @@ describe('HALEndpointService', () => { /* tslint:disable:no-shadowed-variable */ class TestService extends HALEndpointService { - protected linkName = 'test'; + protected linkPath = 'test'; constructor(protected responseCache: ResponseCacheService, protected requestService: RequestService, @@ -29,7 +29,7 @@ describe('HALEndpointService', () => { /* tslint:enable:no-shadowed-variable */ - describe('getEndpointMap', () => { + describe('getRootEndpointMap', () => { beforeEach(() => { responseCache = jasmine.createSpyObj('responseCache', { get: hot('--a-', { @@ -53,13 +53,13 @@ describe('HALEndpointService', () => { }); it('should configure a new RootEndpointRequest', () => { - (service as any).getEndpointMap(); + (service as any).getRootEndpointMap(); const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should return an Observable of the endpoint map', () => { - const result = (service as any).getEndpointMap(); + const result = (service as any).getRootEndpointMap(); const expected = cold('--b-', { b: endpointMap }); expect(result).toBeObservable(expected); }); @@ -74,18 +74,18 @@ describe('HALEndpointService', () => { envConfig ); - spyOn(service as any, 'getEndpointMap').and + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); }); - it('should return the endpoint URL for the service\'s linkName', () => { + it('should return the endpoint URL for the service\'s linkPath', () => { const result = service.getEndpoint(); const expected = cold('--b-', { b: endpointMap.test }); expect(result).toBeObservable(expected); }); - it('should return undefined for a linkName that isn\'t in the endpoint map', () => { - (service as any).linkName = 'unknown'; + it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { + (service as any).linkPath = 'unknown'; const result = service.getEndpoint(); const expected = cold('--b-', { b: undefined }); expect(result).toBeObservable(expected); @@ -103,8 +103,8 @@ describe('HALEndpointService', () => { }); - it('should return undefined as long as getEndpointMap hasn\'t fired', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('----')); const result = service.isEnabledOnRestApi(); @@ -112,8 +112,8 @@ describe('HALEndpointService', () => { expect(result).toBeObservable(expected); }); - it('should return true if the service\'s linkName is in the endpoint map', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return true if the service\'s linkPath is in the endpoint map', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); const result = service.isEnabledOnRestApi(); @@ -121,11 +121,11 @@ describe('HALEndpointService', () => { expect(result).toBeObservable(expected); }); - it('should return false if the service\'s linkName isn\'t in the endpoint map', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); - (service as any).linkName = 'unknown'; + (service as any).linkPath = 'unknown'; const result = service.isEnabledOnRestApi(); const expected = cold('b-c-', { b: undefined, c: false }); expect(result).toBeObservable(expected); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 84587f1eea..c6c3ff284d 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,39 +1,62 @@ import { Observable } from 'rxjs/Observable'; +import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models'; -import { RootEndpointRequest } from '../data/request.models'; +import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models'; +import { EndpointMapRequest, RootEndpointRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; export abstract class HALEndpointService { protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; - protected getEndpointMap(): Observable { - const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig); + protected getRootHref(): string { + return new RESTURLCombiner(this.EnvConfig, '/').toString(); + } + + protected getRootEndpointMap(): Observable { + return this.getEndpointMapAt(this.getRootHref()); + } + + private getEndpointMapAt(href): Observable { + const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); this.requestService.configure(request); return this.responseCache.get(request.href) .map((entry: ResponseCacheEntry) => entry.response) - .filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) - .map((response: RootSuccessResponse) => response.endpointMap) + .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) + .map((response: EndpointMapSuccessResponse) => response.endpointMap) .distinctUntilChanged(); } public getEndpoint(): Observable { - return this.getEndpointMap() - .map((map: EndpointMap) => map[this.linkName]) - .distinctUntilChanged(); + return this.getEndpointAt(...this.linkPath.split('/')); + } + + private getEndpointAt(...path: string[]): Observable { + if (isEmpty(path)) { + path = ['/']; + } + const pipeArguments = path + .map((subPath: string) => [ + flatMap((href: string) => this.getEndpointMapAt(href)), + map((endpointMap: EndpointMap) => endpointMap[subPath]), + ]) + .reduce((combined, thisElement) => [...combined, ...thisElement], []); + return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); } public isEnabledOnRestApi(): Observable { - return this.getEndpointMap() - .map((map: EndpointMap) => isNotEmpty(map[this.linkName])) - .startWith(undefined) - .distinctUntilChanged(); + return this.getRootEndpointMap().pipe( + // TODO this only works when there's no / in linkPath + map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[this.linkPath])), + startWith(undefined), + distinctUntilChanged() + ) } } From 461ba33e0d077ee81a0b1cacca6f2ec9eda4334e Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Mon, 26 Feb 2018 16:06:35 +0100 Subject: [PATCH 02/34] turning search response in to remotedata --- .../search-service/search.service.ts | 64 +++++++++++++++++-- .../builders/remote-data-build.service.ts | 32 ++++++---- .../models/normalized-dspace-object.model.ts | 9 ++- src/app/core/shared/dspace-object.model.ts | 2 +- src/app/core/shared/resource-type.ts | 5 +- 5 files changed, 86 insertions(+), 26 deletions(-) diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index a0af63c28d..94f317103a 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,12 +1,15 @@ import { Inject, Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; -import { map } from 'rxjs/operators'; +import { map, flatMap, tap, filter } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; +import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; +import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; -import { RestResponse } from '../../core/cache/response-cache.models'; +import { RestResponse, SearchSuccessResponse } from '../../core/cache/response-cache.models'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { DebugResponseParsingService } from '../../core/data/debug-response-parsing.service'; import { DSOResponseParsingService } from '../../core/data/dso-response-parsing.service'; @@ -15,6 +18,7 @@ import { PaginatedList } from '../../core/data/paginated-list'; import { ResponseParsingService } from '../../core/data/parsing.service'; import { RemoteData } from '../../core/data/remote-data'; import { GetRequest, EndpointMapRequest, RestRequest } from '../../core/data/request.models'; +import { RequestEntry } from '../../core/data/request.reducer'; import { RequestService } from '../../core/data/request.service'; import { DSpaceRESTV2Response } from '../../core/dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -23,16 +27,19 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Item } from '../../core/shared/item.model'; import { Metadatum } from '../../core/shared/metadatum.model'; import { PageInfo } from '../../core/shared/page-info.model'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { RouteService } from '../../shared/route.service'; +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'; function shuffle(array: any[]) { let i = 0; @@ -95,6 +102,7 @@ export class SearchService extends HALEndpointService implements OnDestroy { @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private routeService: RouteService, private route: ActivatedRoute, + private rdb: RemoteDataBuildService, private router: Router ) { super(); @@ -108,18 +116,62 @@ export class SearchService extends HALEndpointService implements OnDestroy { } search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable>>> { - const searchEndpointUrlObs = this.getEndpoint(); - searchEndpointUrlObs.pipe( + const requestObs = this.getEndpoint().pipe( map((url: string) => { + const args: string[] = []; + + if (isNotEmpty(query)) { + args.push(`query=${query}`); + } + + if (isNotEmpty(scopeId)) { + args.push(`scope=${scopeId}`); + } + + if (isNotEmpty(args)) { + url = new URLCombiner(url, `?${args.join('&')}`).toString(); + } + const request = new GetRequest(this.requestService.generateRequestId(), url); return Object.assign(request, { getResponseParser(): GenericConstructor { return SearchResponseParsingService; } }); + }), + tap((request: RestRequest) => this.requestService.configure(request)), + ); + + const requestEntryObs = requestObs.pipe( + flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + + const responseCacheObs = requestObs.pipe( + flatMap((request: RestRequest) => this.responseCache.get(request.href)) + ); + + const sqrObs = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: SearchSuccessResponse) => response.results) + ); + + const dsoObs = sqrObs.pipe( + map((sqr: SearchQueryResponse) => { + return sqr.objects.map((nsr: NormalizedSearchResult) => + this.rdb.buildSingle(nsr.dspaceObject, NormalizedDSpaceObject)); + }), + flatMap((input: Array>>) => this.rdb.aggregate(input)) + ); + + const payloadObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + return sqr.objects.map((object: NormalizedSearchResult, index: number) => { + return Object.assign({}, object, { + dspaceObject: dsos.payload[index] + }); }) - ).subscribe((request: RestRequest) => this.requestService.configure(request)); - return Observable.of(undefined); + }); + + return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } getConfig(): Observable> { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 9ed43c242b..bb4877980a 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,18 +1,24 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; +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 { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; -import { GetRequest } from '../../data/request.models'; +import { GetRequest, RestRequest } from '../../data/request.models'; import { RequestEntry } from '../../data/request.reducer'; import { RequestService } from '../../data/request.service'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { GenericConstructor } from '../../shared/generic-constructor'; +import { NormalizedDSpaceObject } from '../models/normalized-dspace-object.model'; import { NormalizedObjectFactory } from '../models/normalized-object-factory'; import { CacheableObject } from '../object-cache.reducer'; import { ObjectCacheService } from '../object-cache.service'; -import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models'; +import { DSOSuccessResponse, ErrorResponse, SearchSuccessResponse } from '../response-cache.models'; import { ResponseCacheEntry } from '../response-cache.reducer'; import { ResponseCacheService } from '../response-cache.service'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; @@ -37,7 +43,7 @@ export class RemoteDataBuildService { const requestHrefObs = hrefObs.flatMap((href: string) => this.objectCache.getRequestHrefBySelfLink(href)); - const requestObs = Observable.race( + const requestEntryObs = Observable.race( hrefObs.flatMap((href: string) => this.requestService.getByHref(href)) .filter((entry) => hasValue(entry)), requestHrefObs.flatMap((requestHref) => @@ -80,20 +86,22 @@ export class RemoteDataBuildService { }) .startWith(undefined) .distinctUntilChanged(); - return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs); + return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } - private toRemoteDataObservable(hrefObs: Observable, requestObs: Observable, responseCacheObs: Observable, payloadObs: Observable) { - return Observable.combineLatest(hrefObs, requestObs, responseCacheObs.startWith(undefined), payloadObs, - (href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { + toRemoteDataObservable(requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) { + return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs, + (reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; let isSuccessful: boolean; let error: RemoteDataError; if (hasValue(resEntry) && hasValue(resEntry.response)) { - isSuccessful = resEntry.response.isSuccessful; + isSuccessful = !responsePending && resEntry.response.isSuccessful; const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; - error = new RemoteDataError(resEntry.response.statusCode, errorMessage); + if (hasValue(errorMessage)) { + error = new RemoteDataError(resEntry.response.statusCode, errorMessage); + } } return new RemoteData( @@ -114,7 +122,7 @@ export class RemoteDataBuildService { hrefObs = Observable.of(hrefObs); } - const requestObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href)) + const requestEntryObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href)) .filter((entry) => hasValue(entry)); const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href)) .filter((entry) => hasValue(entry)); @@ -154,10 +162,10 @@ export class RemoteDataBuildService { } }); - return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs); + return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } - build(normalized: TNormalized): TDomain { + build(normalized: TNormalized): TDomain { const links: any = {}; const relationships = getRelationships(normalized.constructor) || []; diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index da42ea5a9b..92174c40f7 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,13 +1,16 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { autoserialize, autoserializeAs } from 'cerialize'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { Metadatum } from '../../shared/metadatum.model'; import { ResourceType } from '../../shared/resource-type'; +import { mapsTo } from '../builders/build-decorators'; import { NormalizedObject } from './normalized-object.model'; /** - * An abstract model class for a DSpaceObject. + * An model class for a DSpaceObject. */ -export abstract class NormalizedDSpaceObject extends NormalizedObject { +@mapsTo(DSpaceObject) +export class NormalizedDSpaceObject extends NormalizedObject { /** * The link to the rest endpoint where this object can be found diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 8f96f2485a..63cedf84f4 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs/Observable'; /** * An abstract model class for a DSpaceObject. */ -export abstract class DSpaceObject implements CacheableObject, ListableObject { +export class DSpaceObject implements CacheableObject, ListableObject { self: string; diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index f3554e18cf..1b73cb0bba 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -1,8 +1,5 @@ -/** - * TODO replace with actual string enum after upgrade to TypeScript 2.4: - * https://github.com/Microsoft/TypeScript/pull/15486 - */ export enum ResourceType { + DSpaceObject = 'dspaceobject', Bundle = 'bundle', Bitstream = 'bitstream', BitstreamFormat = 'bitstreamformat', From 2435b484ce2c02da33b2eb4eb034581e1eef8439 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 2 Mar 2018 16:32:16 +0100 Subject: [PATCH 03/34] search backend fixes #1 --- config/environment.default.js | 21 +++-- src/app/+search-page/search-page.component.ts | 2 +- .../search-result-element-decorator.ts | 17 ++++ .../search-service/search.service.ts | 86 ++++++++++++------- .../builders/remote-data-build.service.ts | 31 +++---- .../normalized-bitstream-format.model.ts | 30 ------- .../cache/models/normalized-object-factory.ts | 5 -- .../cache/models/normalized-object.model.ts | 4 + src/app/core/cache/object-cache.reducer.ts | 2 + src/app/core/cache/object-cache.service.ts | 23 +++-- src/app/core/data/comcol-data.service.ts | 5 +- src/app/core/data/data.service.ts | 11 ++- .../data/search-response-parsing.service.ts | 43 ++++++++-- src/app/core/shared/bitstream-format.model.ts | 11 ++- src/app/core/shared/dspace-object.model.ts | 2 +- .../shared/collection-search-result.model.ts | 4 +- .../shared/community-search-result.model.ts | 4 +- .../shared/item-search-result.model.ts | 2 + ...on-search-result-grid-element.component.ts | 3 +- ...ty-search-result-grid-element.component.ts | 1 + ...on-search-result-list-element.component.ts | 1 + ...ty-search-result-list-element.component.ts | 3 +- .../search-result-list-element.component.ts | 32 ++++--- 23 files changed, 204 insertions(+), 139 deletions(-) create mode 100644 src/app/+search-page/search-service/search-result-element-decorator.ts delete mode 100644 src/app/core/cache/models/normalized-bitstream-format.model.ts diff --git a/config/environment.default.js b/config/environment.default.js index ad7be69b9e..39628c5a68 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -8,13 +8,20 @@ module.exports = { nameSpace: '/' }, // The REST API server settings. - rest: { - ssl: true, - host: 'dspace7.4science.it', - port: 443, - // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - nameSpace: '/dspace-spring-rest/api' - }, + // rest: { + // ssl: true, + // host: 'dspace7.4science.it', + // port: 443, + // // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + // nameSpace: '/dspace-spring-rest/api' + // }, + rest: { + ssl: false, + host: 'dspace7-internal.atmire.com', + port: 80, + // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + nameSpace: '/rest/api' + }, // Caching settings cache: { // NOTE: how long should objects be cached for by default diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index e3426fc96b..389db140e3 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -36,7 +36,7 @@ export class SearchPageComponent implements OnInit, OnDestroy { query: string; scopeObjectRDObs: Observable>; - resultsRDObs: Observable>>>; + resultsRDObs: Observable> | PaginatedList>>>; currentParams = {}; searchOptions: SearchOptions; sortConfig: SortOptions; diff --git a/src/app/+search-page/search-service/search-result-element-decorator.ts b/src/app/+search-page/search-service/search-result-element-decorator.ts new file mode 100644 index 0000000000..545d1b20eb --- /dev/null +++ b/src/app/+search-page/search-service/search-result-element-decorator.ts @@ -0,0 +1,17 @@ +import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; + +const searchResultMap = new Map(); + +export function searchResultFor(domainConstructor: GenericConstructor) { + return function decorator(searchResult: any) { + if (!searchResult) { + return; + } + searchResultMap.set(domainConstructor, searchResult); + }; +} + +export function getSearchResultFor(domainConstructor: GenericConstructor) { + return searchResultMap.get(domainConstructor); +} diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 94f317103a..e3a1ccc11b 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,35 +1,27 @@ import { Inject, Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; -import { map, flatMap, tap, filter } from 'rxjs/operators'; +import { flatMap, map, tap } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; -import { RestResponse, SearchSuccessResponse } from '../../core/cache/response-cache.models'; +import { SearchSuccessResponse } from '../../core/cache/response-cache.models'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; -import { DebugResponseParsingService } from '../../core/data/debug-response-parsing.service'; -import { DSOResponseParsingService } from '../../core/data/dso-response-parsing.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; import { ResponseParsingService } from '../../core/data/parsing.service'; import { RemoteData } from '../../core/data/remote-data'; -import { GetRequest, EndpointMapRequest, RestRequest } from '../../core/data/request.models'; -import { RequestEntry } from '../../core/data/request.reducer'; +import { GetRequest, RestRequest } from '../../core/data/request.models'; import { RequestService } from '../../core/data/request.service'; -import { DSpaceRESTV2Response } from '../../core/dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { Item } from '../../core/shared/item.model'; -import { Metadatum } from '../../core/shared/metadatum.model'; -import { PageInfo } from '../../core/shared/page-info.model'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { RouteService } from '../../shared/route.service'; import { NormalizedSearchResult } from '../normalized-search-result.model'; @@ -40,6 +32,10 @@ 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'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { getSearchResultFor } from './search-result-element-decorator'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { NormalizedItem } from '../../core/cache/models/normalized-item.model'; function shuffle(array: any[]) { let i = 0; @@ -95,16 +91,14 @@ export class SearchService extends HALEndpointService implements OnDestroy { // searchOptions: BehaviorSubject; searchOptions: SearchOptions; - constructor( - protected responseCache: ResponseCacheService, - protected requestService: RequestService, - private itemDataService: ItemDataService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private routeService: RouteService, - private route: ActivatedRoute, - private rdb: RemoteDataBuildService, - private router: Router - ) { + constructor(protected responseCache: ResponseCacheService, + protected requestService: RequestService, + private itemDataService: ItemDataService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private routeService: RouteService, + private route: ActivatedRoute, + private rdb: RemoteDataBuildService, + private router: Router) { super(); const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; @@ -115,7 +109,7 @@ export class SearchService extends HALEndpointService implements OnDestroy { // this.searchOptions = new BehaviorSubject(searchOptions); } - search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable>>> { + search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable> | PaginatedList>>> { const requestObs = this.getEndpoint().pipe( map((url: string) => { const args: string[] = []; @@ -150,25 +144,57 @@ export class SearchService extends HALEndpointService implements OnDestroy { flatMap((request: RestRequest) => this.responseCache.get(request.href)) ); - const sqrObs = responseCacheObs.pipe( + // get search results from response cache + const sqrObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), map((response: SearchSuccessResponse) => response.results) ); - const dsoObs = sqrObs.pipe( + // turn dspace href from search results to effective list of DSpaceObjects + // Turn list of observable remote data DSO's into observable remote data object with list of DSO + const dsoObs: Observable> = sqrObs.pipe( map((sqr: SearchQueryResponse) => { return sqr.objects.map((nsr: NormalizedSearchResult) => - this.rdb.buildSingle(nsr.dspaceObject, NormalizedDSpaceObject)); + this.rdb.buildSingle(nsr.dspaceObject)); }), flatMap((input: Array>>) => this.rdb.aggregate(input)) ); - const payloadObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + // Create search results again with the correct dso objects linked to each result + const tDomainListObs: Observable>> = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { return sqr.objects.map((object: NormalizedSearchResult, index: number) => { - return Object.assign({}, object, { - dspaceObject: dsos.payload[index] - }); - }) + let co = DSpaceObject; + if (dsos.payload[index]) { + const constructor: GenericConstructor = dsos.payload[index].constructor as GenericConstructor; + co = getSearchResultFor(constructor); + return Object.assign(new co(), object, { + dspaceObject: dsos.payload[index] + }); + } else { + return undefined; + } + }); + }); + + const pageInfoObs: Observable = responseCacheObs + .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => { + if (hasValue((entry.response as SearchSuccessResponse).pageInfo)) { + const resPageInfo = (entry.response as SearchSuccessResponse).pageInfo; + if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { + return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); + } else { + return resPageInfo; + } + } + }); + + const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => { + if (hasValue(pageInfo)) { + return new PaginatedList(pageInfo, tDomainList); + } else { + return tDomainList; + } }); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index bb4877980a..45eb6cb46a 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -22,24 +22,19 @@ import { DSOSuccessResponse, ErrorResponse, SearchSuccessResponse } from '../res import { ResponseCacheEntry } from '../response-cache.reducer'; import { ResponseCacheService } from '../response-cache.service'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; +import { NormalizedObject } from '../models/normalized-object.model'; @Injectable() export class RemoteDataBuildService { - constructor( - protected objectCache: ObjectCacheService, - protected responseCache: ResponseCacheService, - protected requestService: RequestService - ) { + constructor(protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService) { } - buildSingle( - hrefObs: string | Observable, - normalizedType: GenericConstructor - ): Observable> { + buildSingle(hrefObs: string | Observable): Observable> { if (typeof hrefObs === 'string') { hrefObs = Observable.of(hrefObs); } - const requestHrefObs = hrefObs.flatMap((href: string) => this.objectCache.getRequestHrefBySelfLink(href)); @@ -59,14 +54,14 @@ export class RemoteDataBuildService { // always use self link if that is cached, only if it isn't, get it via the response. const payloadObs = Observable.combineLatest( - hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink(href, normalizedType)) + hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink(href)) .startWith(undefined), responseCacheObs .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks) .flatMap((resourceSelfLinks: string[]) => { if (isNotEmpty(resourceSelfLinks)) { - return this.objectCache.getBySelfLink(resourceSelfLinks[0], normalizedType); + return this.objectCache.getBySelfLink(resourceSelfLinks[0]); } else { return Observable.of(undefined); } @@ -97,7 +92,7 @@ export class RemoteDataBuildService { let isSuccessful: boolean; let error: RemoteDataError; if (hasValue(resEntry) && hasValue(resEntry.response)) { - isSuccessful = !responsePending && resEntry.response.isSuccessful; + isSuccessful = resEntry.response.isSuccessful; const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { error = new RemoteDataError(resEntry.response.statusCode, errorMessage); @@ -114,10 +109,8 @@ export class RemoteDataBuildService { }); } - buildList( - hrefObs: string | Observable, - normalizedType: GenericConstructor - ): Observable>> { + buildList(hrefObs: string | Observable, + normalizedType: GenericConstructor): Observable>> { if (typeof hrefObs === 'string') { hrefObs = Observable.of(hrefObs); } @@ -181,7 +174,7 @@ export class RemoteDataBuildService { const rdArr = []; normalized[relationship].forEach((href: string) => { - rdArr.push(this.buildSingle(href, resourceConstructor)); + rdArr.push(this.buildSingle(href)); }); if (isList) { @@ -198,7 +191,7 @@ export class RemoteDataBuildService { if (isList) { links[relationship] = this.buildList(normalized[relationship], resourceConstructor); } else { - links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor); + links[relationship] = this.buildSingle(normalized[relationship]); } } } diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts deleted file mode 100644 index bb8b049a1c..0000000000 --- a/src/app/core/cache/models/normalized-bitstream-format.model.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { inheritSerialization, autoserialize } from 'cerialize'; - -import { mapsTo } from '../builders/build-decorators'; - -import { BitstreamFormat } from '../../shared/bitstream-format.model'; -import { NormalizedObject } from './normalized-object.model'; - -@mapsTo(BitstreamFormat) -@inheritSerialization(NormalizedObject) -export class NormalizedBitstreamFormat extends NormalizedObject { - - @autoserialize - shortDescription: string; - - @autoserialize - description: string; - - @autoserialize - mimetype: string; - - @autoserialize - supportLevel: number; - - @autoserialize - internal: boolean; - - @autoserialize - extensions: string; - -} diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 3c67b18b3e..5b13d55ac8 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -1,4 +1,3 @@ -import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { NormalizedBitstream } from './normalized-bitstream.model'; import { NormalizedBundle } from './normalized-bundle.model'; import { NormalizedItem } from './normalized-item.model'; @@ -7,7 +6,6 @@ import { GenericConstructor } from '../../shared/generic-constructor'; import { NormalizedCommunity } from './normalized-community.model'; import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from './normalized-object.model'; -import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor { @@ -15,9 +13,6 @@ export class NormalizedObjectFactory { case ResourceType.Bitstream: { return NormalizedBitstream } - case ResourceType.BitstreamFormat: { - return NormalizedBitstreamFormat - } case ResourceType.Bundle: { return NormalizedBundle } diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts index b26bd90b2a..e98081d68a 100644 --- a/src/app/core/cache/models/normalized-object.model.ts +++ b/src/app/core/cache/models/normalized-object.model.ts @@ -1,5 +1,6 @@ import { CacheableObject } from '../object-cache.reducer'; import { autoserialize } from 'cerialize'; +import { ResourceType } from '../../shared/resource-type'; /** * An abstract model class for a NormalizedObject. */ @@ -17,6 +18,9 @@ export abstract class NormalizedObject implements CacheableObject { @autoserialize uuid: string; + @autoserialize + type: ResourceType; + @autoserialize _links: { [name: string]: string diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 39c623deed..1ae3a190a4 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -4,6 +4,7 @@ import { } from './object-cache.actions'; import { hasValue } from '../../shared/empty.util'; import { CacheEntry } from './cache-entry'; +import { ResourceType } from '../shared/resource-type'; export enum DirtyType { Created = 'Created', @@ -19,6 +20,7 @@ export enum DirtyType { export interface CacheableObject { uuid?: string; self: string; + type: ResourceType; // isNew: boolean; // dirtyType: DirtyType; // hasDirtyAttributes: boolean; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index ae41c38fbe..b747880dc3 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -10,6 +10,9 @@ import { hasNoValue } from '../../shared/empty.util'; import { GenericConstructor } from '../shared/generic-constructor'; import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; +import { Item } from '../shared/item.model'; +import { NormalizedObjectFactory } from './models/normalized-object-factory'; +import { NormalizedObject } from './models/normalized-object.model'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); @@ -24,9 +27,8 @@ function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector - ) { } + constructor(private store: Store) { + } /** * Add an object to the cache @@ -70,14 +72,17 @@ export class ObjectCacheService { * @return Observable * An observable of the requested object */ - getByUUID(uuid: string, type: GenericConstructor): Observable { + getByUUID(uuid: string): Observable { return this.store.select(selfLinkFromUuidSelector(uuid)) - .flatMap((selfLink: string) => this.getBySelfLink(selfLink, type)) + .flatMap((selfLink: string) => this.getBySelfLink(selfLink)) } - getBySelfLink(selfLink: string, type: GenericConstructor): Observable { + getBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink) - .map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T); + .map((entry: ObjectCacheEntry) => { + const type: GenericConstructor= NormalizedObjectFactory.getConstructor(entry.data.type); + return Object.assign(new type(), entry.data) as T + }); } private getEntry(selfLink: string): Observable { @@ -116,9 +121,9 @@ export class ObjectCacheService { * The type of the objects to get * @return Observable> */ - getList(selfLinks: string[], type: GenericConstructor): Observable { + getList(selfLinks: string[], type: GenericConstructor): Observable { return Observable.combineLatest( - selfLinks.map((selfLink: string) => this.getBySelfLink(selfLink, type)) + selfLinks.map((selfLink: string) => this.getBySelfLink(selfLink)) ); } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index cc9ef4ad60..24c019c611 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -9,8 +9,9 @@ import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; import { FindByIDRequest } from './request.models'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; -export abstract class ComColDataService extends DataService { +export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; @@ -47,7 +48,7 @@ export abstract class ComColDataService Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), successResponse - .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) + .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID)) .map((nc: NormalizedCommunity) => nc._links[this.linkPath]) .filter((href) => isNotEmpty(href)) ).distinctUntilChanged(); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ccdbbea5b9..792c8acb94 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -13,8 +13,9 @@ import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; import { RequestService } from './request.service'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; -export abstract class DataService extends HALEndpointService { +export abstract class DataService extends HALEndpointService { protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; @@ -22,9 +23,7 @@ export abstract class DataService protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; - constructor( - protected normalizedResourceType: GenericConstructor, - ) { + constructor(protected normalizedResourceType: GenericConstructor,) { super(); } @@ -95,12 +94,12 @@ export abstract class DataService this.requestService.configure(request); }); - return this.rdbService.buildSingle(hrefObs, this.normalizedResourceType); + return this.rdbService.buildSingle(hrefObs); } findByHref(href: string): Observable> { this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href)); - return this.rdbService.buildSingle(href, this.normalizedResourceType); + return this.rdbService.buildSingle(href); } // TODO implement, after the structure of the REST server's POST response is finalized diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index bd64101397..886da34200 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -1,23 +1,35 @@ import { Injectable } from '@angular/core'; -import { - DSOSuccessResponse, RestResponse, - SearchSuccessResponse -} from '../cache/response-cache.models'; +import { RestResponse, SearchSuccessResponse } from '../cache/response-cache.models'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { PageInfo } from '../shared/page-info.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { isNotEmpty } from '../../shared/empty.util'; import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; +import { Metadatum } from '../shared/metadatum.model'; @Injectable() export class SearchResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) {} + constructor(private dsoParser: DSOResponseParsingService) { + } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; + const hitHighlights = payload._embedded.objects + .map((object) => object.hitHighlights) + .map((hhObject) => { + if (hhObject) { + return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), { + key: key, + value: hhObject[key].join('...') + })) + } else { + return undefined; + } + }); + const dsoSelfLinks = payload._embedded.objects .map((object) => object._embedded.dspaceObject) // we don't need embedded collections, bitstreamformats, etc for search results. @@ -32,11 +44,24 @@ export class SearchResponseParsingService implements ResponseParsingService { .reduce((combined, thisElement) => [...combined, ...thisElement], []); const objects = payload._embedded.objects - .map((object, index) => Object.assign({}, object, { dspaceObject: dsoSelfLinks[index] })); - + .map((object, index) => Object.assign({}, object, { + dspaceObject: dsoSelfLinks[index], + hitHighlights: hitHighlights[index], + // we don't need embedded collections, bitstreamformats, etc for search results. + // And parsing them all takes up a lot of time. Throw them away to improve performance + // until objs until partial results are supported by the rest api + _embedded: undefined + })); payload.objects = objects; const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); - return new SearchSuccessResponse(deserialized, data.statusCode, undefined); + return new SearchSuccessResponse(deserialized, data.statusCode, this.processPageInfo(data.payload.page)); } + protected processPageInfo(pageObj: any): PageInfo { + if (isNotEmpty(pageObj)) { + return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + } else { + return undefined; + } + } } diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index c0f6be29c9..b85d9e2053 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -1,17 +1,24 @@ -import { DSpaceObject } from './dspace-object.model'; -export class BitstreamFormat extends DSpaceObject { +import { autoserialize } from 'cerialize'; +export class BitstreamFormat { + + @autoserialize shortDescription: string; + @autoserialize description: string; + @autoserialize mimetype: string; + @autoserialize supportLevel: number; + @autoserialize internal: boolean; + @autoserialize extensions: string; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 63cedf84f4..5e62e3e321 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs/Observable'; /** * An abstract model class for a DSpaceObject. */ -export class DSpaceObject implements CacheableObject, ListableObject { +export class DSpaceObject implements CacheableObject, ListableObject { self: string; diff --git a/src/app/shared/object-collection/shared/collection-search-result.model.ts b/src/app/shared/object-collection/shared/collection-search-result.model.ts index 63b6a0d37a..5e9afc59e5 100644 --- a/src/app/shared/object-collection/shared/collection-search-result.model.ts +++ b/src/app/shared/object-collection/shared/collection-search-result.model.ts @@ -1,5 +1,7 @@ -import { Collection } from '../../../core/shared/collection.model'; import { SearchResult } from '../../../+search-page/search-result.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator'; +@searchResultFor(Collection) export class CollectionSearchResult extends SearchResult { } diff --git a/src/app/shared/object-collection/shared/community-search-result.model.ts b/src/app/shared/object-collection/shared/community-search-result.model.ts index 79ea34b6cd..8e371c182c 100644 --- a/src/app/shared/object-collection/shared/community-search-result.model.ts +++ b/src/app/shared/object-collection/shared/community-search-result.model.ts @@ -1,5 +1,7 @@ -import { SearchResult } from '../../../+search-page/search-result.model'; import { Community } from '../../../core/shared/community.model'; +import { SearchResult } from '../../../+search-page/search-result.model'; +import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator'; +@searchResultFor(Community) export class CommunitySearchResult extends SearchResult { } diff --git a/src/app/shared/object-collection/shared/item-search-result.model.ts b/src/app/shared/object-collection/shared/item-search-result.model.ts index d9af3539a0..f20b3db1e3 100644 --- a/src/app/shared/object-collection/shared/item-search-result.model.ts +++ b/src/app/shared/object-collection/shared/item-search-result.model.ts @@ -1,5 +1,7 @@ import { SearchResult } from '../../../+search-page/search-result.model'; import { Item } from '../../../core/shared/item.model'; +import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator'; +@searchResultFor(Item) export class ItemSearchResult extends SearchResult { } diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts index e5747a1243..5d814ac7e5 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts @@ -2,10 +2,11 @@ import { Component } from '@angular/core'; import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator'; -import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; + import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { Collection } from '../../../../core/shared/collection.model'; import { ViewMode } from '../../../../+search-page/search-options.model'; +import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; @Component({ selector: 'ds-collection-search-result-grid-element', diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts index d08286ff2e..f011e49eda 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; + import { Community } from '../../../../core/shared/community.model'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts index 9a462c124e..746e700f05 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; + import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { Collection } from '../../../../core/shared/collection.model'; import { ViewMode } from '../../../../+search-page/search-options.model'; diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts index 5664e840e3..2ca89fc9c5 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts @@ -1,10 +1,11 @@ import { Component } from '@angular/core'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; -import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; + import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { Community } from '../../../../core/shared/community.model'; import { ViewMode } from '../../../../+search-page/search-options.model'; +import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; @Component({ selector: 'ds-community-search-result-list-element', diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 9675a58a1e..31e31c72a8 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -3,7 +3,7 @@ import { Component, Inject } from '@angular/core'; import { SearchResult } from '../../../+search-page/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { Metadatum } from '../../../core/shared/metadatum.model'; -import { isEmpty, hasNoValue } from '../../empty.util'; +import { isEmpty, hasNoValue, isNotEmpty } from '../../empty.util'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { Observable } from 'rxjs/Observable'; @@ -24,13 +24,15 @@ export class SearchResultListElementComponent, K exten getValues(keys: string[]): string[] { const results: string[] = new Array(); - this.object.hitHighlights.forEach( - (md: Metadatum) => { - if (keys.indexOf(md.key) > -1) { - results.push(md.value); + if (isNotEmpty(this.object.hitHighlights)) { + this.object.hitHighlights.forEach( + (md: Metadatum) => { + if (keys.indexOf(md.key) > -1) { + results.push(md.value); + } } - } - ); + ); + } if (isEmpty(results)) { this.dso.filterMetadata(keys).forEach( (md: Metadatum) => { @@ -43,14 +45,16 @@ export class SearchResultListElementComponent, K exten getFirstValue(key: string): string { let result: string; - this.object.hitHighlights.some( - (md: Metadatum) => { - if (key === md.key) { - result = md.value; - return true; + if (isNotEmpty(this.object.hitHighlights)) { + this.object.hitHighlights.some( + (md: Metadatum) => { + if (key === md.key) { + result = md.value; + return true; + } } - } - ); + ); + } if (hasNoValue(result)) { result = this.dso.findMetadata(key); } From b8f4769f1e8293d2e47b8847be7206c63e429dc8 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Mon, 5 Mar 2018 14:51:49 +0100 Subject: [PATCH 04/34] reinstated 4science test server --- config/environment.default.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 39628c5a68..ad7be69b9e 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -8,20 +8,13 @@ module.exports = { nameSpace: '/' }, // The REST API server settings. - // rest: { - // ssl: true, - // host: 'dspace7.4science.it', - // port: 443, - // // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - // nameSpace: '/dspace-spring-rest/api' - // }, - rest: { - ssl: false, - host: 'dspace7-internal.atmire.com', - port: 80, - // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - nameSpace: '/rest/api' - }, + rest: { + ssl: true, + host: 'dspace7.4science.it', + port: 443, + // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + nameSpace: '/dspace-spring-rest/api' + }, // Caching settings cache: { // NOTE: how long should objects be cached for by default From acf85577ee650d2de561f5274aec92067f84a3a1 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Wed, 7 Mar 2018 11:44:08 +0100 Subject: [PATCH 05/34] fixed AoT build and search settings --- config/environment.default.js | 21 ++++++++++++------- .../search-filter/search-filter.component.ts | 2 -- src/app/+search-page/search-page.component.ts | 5 +++-- .../search-service/search.service.ts | 12 +++++++++-- .../search-settings.component.html | 16 +++++++------- .../search-settings.component.ts | 6 +++--- .../builders/remote-data-build.service.ts | 8 +++---- .../core/cache/models/sort-options.model.ts | 7 +++---- src/app/core/cache/object-cache.reducer.ts | 2 +- .../core/cache/object-cache.service.spec.ts | 10 +++++---- src/app/core/cache/object-cache.service.ts | 2 +- src/app/core/config/config.service.ts | 6 +----- src/app/core/data/collection-data.service.ts | 2 +- src/app/core/data/comcol-data.service.spec.ts | 7 +++---- src/app/core/data/community-data.service.ts | 2 +- src/app/core/data/data.service.ts | 13 ++---------- src/app/core/data/item-data.service.ts | 2 +- .../pagination/pagination.component.html | 2 +- .../shared/pagination/pagination.component.ts | 4 ++-- src/app/shared/utils/enum-keys-pipe.ts | 2 ++ 20 files changed, 66 insertions(+), 65 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index ad7be69b9e..39628c5a68 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -8,13 +8,20 @@ module.exports = { nameSpace: '/' }, // The REST API server settings. - rest: { - ssl: true, - host: 'dspace7.4science.it', - port: 443, - // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - nameSpace: '/dspace-spring-rest/api' - }, + // rest: { + // ssl: true, + // host: 'dspace7.4science.it', + // port: 443, + // // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + // nameSpace: '/dspace-spring-rest/api' + // }, + rest: { + ssl: false, + host: 'dspace7-internal.atmire.com', + port: 80, + // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + nameSpace: '/rest/api' + }, // Caching settings cache: { // NOTE: how long should objects be cached for by default diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index 08d72da984..ddb21236c5 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -6,8 +6,6 @@ import { FacetValue } from '../../search-service/facet-value.model'; import { SearchFilterService } from './search-filter.service'; import { Observable } from 'rxjs/Observable'; import { slide } from '../../../shared/animations/slide'; -import { RouteService } from '../../../shared/route.service'; -import { first } from 'rxjs/operator/first'; /** * This component renders a simple item page. diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 389db140e3..9c5c9a7462 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -89,14 +89,15 @@ export class SearchPageComponent implements OnInit, OnDestroy { } } - const sortDirection = +params.sortDirection || this.searchOptions.sort.direction; + const sortDirection = params.sortDirection || this.searchOptions.sort.direction; + const sortField = params.sortField || this.searchOptions.sort.field; const pagination = Object.assign({}, this.searchOptions.pagination, { currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions} ); const sort = Object.assign({}, this.searchOptions.sort, - { direction: sortDirection, field: params.sortField } + { direction: sortDirection, field: sortField } ); this.updateSearchResults({ diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index e3a1ccc11b..f343adc38b 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -35,7 +35,6 @@ import { SearchQueryResponse } from './search-query-response.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { getSearchResultFor } from './search-result-element-decorator'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { NormalizedItem } from '../../core/cache/models/normalized-item.model'; function shuffle(array: any[]) { let i = 0; @@ -93,7 +92,6 @@ export class SearchService extends HALEndpointService implements OnDestroy { constructor(protected responseCache: ResponseCacheService, protected requestService: RequestService, - private itemDataService: ItemDataService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private routeService: RouteService, private route: ActivatedRoute, @@ -122,6 +120,16 @@ export class SearchService extends HALEndpointService implements OnDestroy { args.push(`scope=${scopeId}`); } + if (isNotEmpty(searchOptions)) { + if (isNotEmpty(searchOptions.sort)) { + args.push(`sort=${searchOptions.sort.field},${searchOptions.sort.direction}`); + } + if (isNotEmpty(searchOptions.pagination)) { + args.push(`page=${searchOptions.pagination.currentPage}`); + args.push(`size=${searchOptions.pagination.pageSize}`); + } + } + if (isNotEmpty(args)) { url = new URLCombiner(url, `?${args.join('&')}`).toString(); } diff --git a/src/app/+search-page/search-settings/search-settings.component.html b/src/app/+search-page/search-settings/search-settings.component.html index 4ee0812602..b0d3294e30 100644 --- a/src/app/+search-page/search-settings/search-settings.component.html +++ b/src/app/+search-page/search-settings/search-settings.component.html @@ -2,21 +2,21 @@
{{ 'search.sidebar.settings.sort-by' | translate}}
-
+
{{ 'search.sidebar.settings.rpp' | translate}}
diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index ad6aeb21dd..bc1fb096fd 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { SearchService } from '../search-service/search.service'; import { SearchOptions, ViewMode } from '../search-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; @@ -7,7 +7,7 @@ import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; @Component({ selector: 'ds-search-settings', styleUrls: ['./search-settings.component.scss'], - templateUrl: './search-settings.component.html', + templateUrl: './search-settings.component.html' }) export class SearchSettingsComponent implements OnInit { @@ -48,7 +48,7 @@ export class SearchSettingsComponent implements OnInit { this.scope = params.scope; this.page = +params.page || this.searchOptions.pagination.currentPage; this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize; - this.direction = +params.sortDirection || this.searchOptions.sort.direction; + this.direction = params.sortDirection || this.searchOptions.sort.direction; if (params.view === ViewMode.Grid) { this.pageSizeOptions = this.gridPageSizeOptions; } else { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 45eb6cb46a..af2c80c9aa 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -109,8 +109,7 @@ export class RemoteDataBuildService { }); } - buildList(hrefObs: string | Observable, - normalizedType: GenericConstructor): Observable>> { + buildList(hrefObs: string | Observable): Observable>> { if (typeof hrefObs === 'string') { hrefObs = Observable.of(hrefObs); } @@ -124,7 +123,7 @@ export class RemoteDataBuildService { .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks) .flatMap((resourceUUIDs: string[]) => { - return this.objectCache.getList(resourceUUIDs, normalizedType) + return this.objectCache.getList(resourceUUIDs) .map((normList: TNormalized[]) => { return normList.map((normalized: TNormalized) => { return this.build(normalized); @@ -166,7 +165,6 @@ export class RemoteDataBuildService { relationships.forEach((relationship: string) => { if (hasValue(normalized[relationship])) { const { resourceType, isList } = getRelationMetadata(normalized, relationship); - const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType); if (Array.isArray(normalized[relationship])) { normalized[relationship].forEach((href: string) => { this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href)) @@ -189,7 +187,7 @@ export class RemoteDataBuildService { // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), // but it should still be built as a list if (isList) { - links[relationship] = this.buildList(normalized[relationship], resourceConstructor); + links[relationship] = this.buildList(normalized[relationship]); } else { links[relationship] = this.buildSingle(normalized[relationship]); } diff --git a/src/app/core/cache/models/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts index ad639bf853..751b72b399 100644 --- a/src/app/core/cache/models/sort-options.model.ts +++ b/src/app/core/cache/models/sort-options.model.ts @@ -1,11 +1,10 @@ export enum SortDirection { - Ascending, - Descending + Ascending = 'ASC', + Descending = 'DESC' } export class SortOptions { - - constructor(public field: string = 'name', public direction: SortDirection = SortDirection.Ascending) { + constructor(public field: string = 'dc.title', public direction: SortDirection = SortDirection.Ascending) { } } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 1ae3a190a4..3a1830e14a 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -20,7 +20,7 @@ export enum DirtyType { export interface CacheableObject { uuid?: string; self: string; - type: ResourceType; + type?: ResourceType; // isNew: boolean; // dirtyType: DirtyType; // hasDirtyAttributes: boolean; diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 2cf7eebd0a..86b8a3efdf 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -5,11 +5,13 @@ import { ObjectCacheService } from './object-cache.service'; import { CacheableObject } from './object-cache.reducer'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { CoreState } from '../core.reducers'; +import { ResourceType } from '../shared/resource-type'; class TestClass implements CacheableObject { constructor( public self: string, - public foo: string + public foo: string, + public type = ResourceType.Item ) { } test(): string { @@ -65,7 +67,7 @@ describe('ObjectCacheService', () => { let testObj: any; // due to the implementation of spyOn above, this subscribe will be synchronous - service.getBySelfLink(selfLink, TestClass).take(1).subscribe((o) => testObj = o); + service.getBySelfLink(selfLink).take(1).subscribe((o) => testObj = o); expect(testObj.self).toBe(selfLink); expect(testObj.foo).toBe('bar'); // this only works if testObj is an instance of TestClass @@ -76,7 +78,7 @@ describe('ObjectCacheService', () => { spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); let getObsHasFired = false; - const subscription = service.getBySelfLink(selfLink, TestClass).subscribe((o) => getObsHasFired = true); + const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true); expect(getObsHasFired).toBe(false); subscription.unsubscribe(); }); @@ -87,7 +89,7 @@ describe('ObjectCacheService', () => { spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(new TestClass(selfLink, 'bar'))); let testObjs: any[]; - service.getList([selfLink, selfLink], TestClass).take(1).subscribe((arr) => testObjs = arr); + service.getList([selfLink, selfLink]).take(1).subscribe((arr) => testObjs = arr); expect(testObjs[0].self).toBe(selfLink); expect(testObjs[0].foo).toBe('bar'); expect(testObjs[0].test()).toBe('bar' + selfLink); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index b747880dc3..9344f4d5f0 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -121,7 +121,7 @@ export class ObjectCacheService { * The type of the objects to get * @return Observable> */ - getList(selfLinks: string[], type: GenericConstructor): Observable { + getList(selfLinks: string[]): Observable { return Observable.combineLatest( selfLinks.map((selfLink: string) => this.getBySelfLink(selfLink)) ); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index a8c2bc46bf..f1750bca84 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -58,11 +58,7 @@ export abstract class ConfigService extends HALEndpointService { } if (hasValue(options.sort)) { - let direction = 'asc'; - if (options.sort.direction === 1) { - direction = 'desc'; - } - args.push(`sort=${options.sort.field},${direction}`); + args.push(`sort=${options.sort.field},${options.sort.direction}`); } if (isNotEmpty(args)) { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index a0f246aedc..a675b7254d 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -24,6 +24,6 @@ export class CollectionDataService extends ComColDataService { @@ -33,7 +32,7 @@ class TestService extends ComColDataService { protected cds: CommunityDataService, protected objectCache: ObjectCacheService ) { - super(NormalizedTestObject); + super(); } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index dce67e936f..4cb44f4296 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -24,6 +24,6 @@ export class CommunityDataService extends ComColDataService protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; - constructor(protected normalizedResourceType: GenericConstructor,) { - super(); - } - public abstract getScopedEndpoint(scope: string): Observable protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable { @@ -49,11 +44,7 @@ export abstract class DataService } if (hasValue(options.sort)) { - let direction = 'asc'; - if (options.sort.direction === 1) { - direction = 'desc'; - } - args.push(`sort=${options.sort.field},${direction}`); + args.push(`sort=${options.sort.field},${options.sort.direction}`); } if (isNotEmpty(args)) { @@ -75,7 +66,7 @@ export abstract class DataService this.requestService.configure(request); }); - return this.rdbService.buildList(hrefObs, this.normalizedResourceType) as Observable>>; + return this.rdbService.buildList(hrefObs) as Observable>>; } getFindByIDHref(endpoint, resourceID): string { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 155ba357c6..0e6fcbfd33 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -27,7 +27,7 @@ export class ItemDataService extends DataService { @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private bs: BrowseService ) { - super(NormalizedItem); + super(); } public getScopedEndpoint(scopeID: string): Observable { diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 062209f4bb..0ad812a6b6 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -12,7 +12,7 @@ - +
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index e2af2fd06c..2fa70ddd18 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -399,8 +399,8 @@ export class PaginationComponent implements OnDestroy, OnInit { } const sortDirection = this.currentQueryParams.sortDirection; - if (this.sortDirection !== +sortDirection) { - this.setSortDirection(+sortDirection); + if (this.sortDirection !== sortDirection) { + this.setSortDirection(sortDirection); } const sortField = this.currentQueryParams.sortField; diff --git a/src/app/shared/utils/enum-keys-pipe.ts b/src/app/shared/utils/enum-keys-pipe.ts index 45683c3323..82893b886f 100644 --- a/src/app/shared/utils/enum-keys-pipe.ts +++ b/src/app/shared/utils/enum-keys-pipe.ts @@ -7,6 +7,8 @@ export class EnumKeysPipe implements PipeTransform { for (const enumMember in value) { if (!isNaN(parseInt(enumMember, 10))) { keys.push({ key: +enumMember, value: value[enumMember] }); + } else { + keys.push({ key: enumMember, value: value[enumMember] }); } } return keys; From 0d18d08d22ddad98119e9ee26bb373e08d71bba0 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 8 Mar 2018 12:17:31 +0100 Subject: [PATCH 06/34] fixed some tests --- .../search-facet-filter.component.spec.ts | 4 +- .../search-filter.service.spec.ts | 4 +- .../search-service/search.service.spec.ts | 11 +++++ .../search-service/search.service.ts | 9 ++-- .../core/cache/object-cache.service.spec.ts | 44 +++++++------------ src/app/core/data/comcol-data.service.spec.ts | 2 +- .../pagination/pagination.component.spec.ts | 4 +- .../view-mode-switch.component.spec.ts | 15 ++++++- 8 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index d6c6da3051..bf069eee60 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -96,8 +96,8 @@ describe('SearchFacetFilterComponent', () => { link = comp.getSearchLink(); }); - it('should return the value of the uiSearchRoute variable in the filter service', () => { - expect(link).toEqual(filterService.uiSearchRoute); + it('should return the value of the searchLink variable in the filter service', () => { + expect(link).toEqual(filterService.searchLink); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 6dcf8d73b8..853f8b0f68 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -46,7 +46,7 @@ describe('SearchFilterService', () => { }; const searchServiceStub: any = { - searchLink: '/search' + uiSearchRoute: '/search' }; beforeEach(() => { @@ -199,7 +199,7 @@ describe('SearchFilterService', () => { }); it('should return the value of uiSearchRoute in the search service', () => { - expect(link).toEqual(searchServiceStub.searchLink); + expect(link).toEqual(searchServiceStub.uiSearchRoute); }); }); }); diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 65af3231f9..a0d4b1f049 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -8,6 +8,12 @@ import { SearchService } from './search.service'; import { ItemDataService } from './../../core/data/item-data.service'; 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 } 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'; @Component({ template: '' }) class DummyComponent { } @@ -29,6 +35,11 @@ describe('SearchService', () => { providers: [ { provide: ItemDataService, useValue: {} }, { provide: RouteService, useValue: {} }, + { provide: ResponseCacheService, useValue: {} }, + { provide: RequestService, useValue: {} }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: GLOBAL_CONFIG, useValue: {} }, SearchService ], }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index f343adc38b..2dac0bd340 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -6,12 +6,10 @@ import { ViewMode } from '../../+search-page/search-options.model'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; -import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SearchSuccessResponse } from '../../core/cache/response-cache.models'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; -import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; import { ResponseParsingService } from '../../core/data/parsing.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -125,11 +123,10 @@ export class SearchService extends HALEndpointService implements OnDestroy { args.push(`sort=${searchOptions.sort.field},${searchOptions.sort.direction}`); } if (isNotEmpty(searchOptions.pagination)) { - args.push(`page=${searchOptions.pagination.currentPage}`); + args.push(`page=${searchOptions.pagination.currentPage - 1}`); args.push(`size=${searchOptions.pagination.pageSize}`); } } - if (isNotEmpty(args)) { url = new URLCombiner(url, `?${args.join('&')}`).toString(); } @@ -233,8 +230,8 @@ export class SearchService extends HALEndpointService implements OnDestroy { payload.push({ value: value, count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count - search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value - }); + search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value)} + ); } } const requestPending = false; diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 86b8a3efdf..80a9121544 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -2,22 +2,10 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { ObjectCacheService } from './object-cache.service'; -import { CacheableObject } from './object-cache.reducer'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { CoreState } from '../core.reducers'; import { ResourceType } from '../shared/resource-type'; - -class TestClass implements CacheableObject { - constructor( - public self: string, - public foo: string, - public type = ResourceType.Item - ) { } - - test(): string { - return this.foo + this.self; - } -} +import { NormalizedItem } from './models/normalized-item.model'; describe('ObjectCacheService', () => { let service: ObjectCacheService; @@ -28,7 +16,7 @@ describe('ObjectCacheService', () => { const msToLive = 900000; const objectToCache = { self: selfLink, - foo: 'bar' + type: ResourceType.Item }; const cacheEntry = { data: objectToCache, @@ -65,13 +53,13 @@ describe('ObjectCacheService', () => { it('should return an observable of the cached object with the specified self link and type', () => { spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); - let testObj: any; // due to the implementation of spyOn above, this subscribe will be synchronous - service.getBySelfLink(selfLink).take(1).subscribe((o) => testObj = o); - expect(testObj.self).toBe(selfLink); - expect(testObj.foo).toBe('bar'); - // this only works if testObj is an instance of TestClass - expect(testObj.test()).toBe('bar' + selfLink); + service.getBySelfLink(selfLink).take(1).subscribe((o) => { + expect(o.self).toBe(selfLink); + // this only works if testObj is an instance of TestClass + expect(o instanceof NormalizedItem).toBeTruthy(); + } + ); }); it('should not return a cached object that has exceeded its time to live', () => { @@ -86,16 +74,14 @@ describe('ObjectCacheService', () => { describe('getList', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => { - spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(new TestClass(selfLink, 'bar'))); + const item = new NormalizedItem(); + item.self = selfLink; + spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(item)); - let testObjs: any[]; - service.getList([selfLink, selfLink]).take(1).subscribe((arr) => testObjs = arr); - expect(testObjs[0].self).toBe(selfLink); - expect(testObjs[0].foo).toBe('bar'); - expect(testObjs[0].test()).toBe('bar' + selfLink); - expect(testObjs[1].self).toBe(selfLink); - expect(testObjs[1].foo).toBe('bar'); - expect(testObjs[1].test()).toBe('bar' + selfLink); + service.getList([selfLink, selfLink]).take(1).subscribe((arr) => { + expect(arr[0].self).toBe(selfLink); + expect(arr[0] instanceof NormalizedItem).toBeTruthy(); + }); }); }); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 2c38325a4f..cf43482ba5 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -126,7 +126,7 @@ describe('ComColDataService', () => { it('should fetch the scope Community from the cache', () => { scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.flush(); - expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID, NormalizedCommunity); + expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID); }); it('should return the endpoint to fetch resources within the given scope', () => { diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index a4b9e5fcea..b108074893 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -271,7 +271,7 @@ describe('Pagination component', () => { changePage(testFixture, 3); tick(); - expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 0, sortField: 'name' } }); + expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 'ASC', sortField: 'dc.title' } }); })); @@ -282,7 +282,7 @@ describe('Pagination component', () => { changePageSize(testFixture, '20'); tick(); - expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 1, pageSize: 20, sortDirection: 0, sortField: 'name' } }); + expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 1, pageSize: 20, sortDirection: 'ASC', sortField: 'dc.title' } }); })); it('should set correct values', fakeAsync(() => { diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index 541b1ed4c3..3923fec0a6 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -11,6 +11,12 @@ import { ItemDataService } from './../../core/data/item-data.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { ViewMode } from '../../+search-page/search-options.model'; import { RouteService } from '../route.service'; +import { ResponseCacheService } from '../../core/cache/response-cache.service'; +import { RequestService } from '../../core/data/request.service'; +import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; +import { ActivatedRoute } from '@angular/router'; +import { GLOBAL_CONFIG } from '../../../config'; +import { ActivatedRouteStub } from '../testing/active-router-stub'; @Component({ template: '' }) class DummyComponent { } @@ -21,7 +27,7 @@ describe('ViewModeSwitchComponent', () => { let searchService: SearchService; let listButton: HTMLElement; let gridButton: HTMLElement; - + let route = new ActivatedRouteStub(); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -42,6 +48,11 @@ describe('ViewModeSwitchComponent', () => { providers: [ { provide: ItemDataService, useValue: {} }, { provide: RouteService, useValue: {} }, + { provide: ResponseCacheService, useValue: {} }, + { provide: RequestService, useValue: {} }, + { provide: ActivatedRoute, useValue: route }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: GLOBAL_CONFIG, useValue: {} }, SearchService ], }).compileComponents(); @@ -59,6 +70,7 @@ describe('ViewModeSwitchComponent', () => { it('should set list button as active when on list mode', fakeAsync(() => { searchService.setViewMode(ViewMode.List); + route = new ActivatedRouteStub([{view: ViewMode.List}]) tick(); fixture.detectChanges(); expect(comp.currentMode).toBe(ViewMode.List); @@ -68,6 +80,7 @@ describe('ViewModeSwitchComponent', () => { it('should set grid button as active when on grid mode', fakeAsync(() => { searchService.setViewMode(ViewMode.Grid); + route = new ActivatedRouteStub([{view: ViewMode.Grid}]) tick(); fixture.detectChanges(); expect(comp.currentMode).toBe(ViewMode.Grid); From e374f9ec1f67f7d3ba2b3a5c2fdf547e516311f4 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 8 Mar 2018 15:30:16 +0100 Subject: [PATCH 07/34] reinstated 4Science rest API --- config/environment.default.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 39628c5a68..ad7be69b9e 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -8,20 +8,13 @@ module.exports = { nameSpace: '/' }, // The REST API server settings. - // rest: { - // ssl: true, - // host: 'dspace7.4science.it', - // port: 443, - // // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - // nameSpace: '/dspace-spring-rest/api' - // }, - rest: { - ssl: false, - host: 'dspace7-internal.atmire.com', - port: 80, - // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - nameSpace: '/rest/api' - }, + rest: { + ssl: true, + host: 'dspace7.4science.it', + port: 443, + // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + nameSpace: '/dspace-spring-rest/api' + }, // Caching settings cache: { // NOTE: how long should objects be cached for by default From 6c2a249fddab7b5727f1542be718609fa1c787a5 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 15 Mar 2018 16:15:02 +0100 Subject: [PATCH 08/34] test fixes --- karma.conf.js | 2 +- .../search-service/search.service.spec.ts | 134 ++++++++++++------ .../search-service/search.service.ts | 21 +-- src/app/core/data/request.models.ts | 7 - .../core/shared/hal-endpoint.service.spec.ts | 30 ++-- src/app/core/shared/hal-endpoint.service.ts | 2 +- ...on-search-result-grid-element.component.ts | 2 - ...ty-search-result-grid-element.component.ts | 2 - src/app/shared/testing/active-router-stub.ts | 4 +- src/app/shared/testing/search-service-stub.ts | 32 +++++ .../view-mode-switch.component.spec.ts | 28 +--- 11 files changed, 164 insertions(+), 100 deletions(-) create mode 100644 src/app/shared/testing/search-service-stub.ts diff --git a/karma.conf.js b/karma.conf.js index 073ce7040b..456c2ecd99 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -51,7 +51,7 @@ module.exports = function (config) { */ files: [{ pattern: './spec-bundle.js', - watched: false + watched: false, }], /* diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index a0d4b1f049..f8a1d73ae1 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -10,59 +10,111 @@ 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 } from '@angular/router'; +import { ActivatedRoute, Router } 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'; +import { RouterStub } from '../../shared/testing/router-stub'; @Component({ template: '' }) -class DummyComponent { } +class DummyComponent { +} describe('SearchService', () => { - let searchService: SearchService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - CommonModule, - RouterTestingModule.withRoutes([ - { path: 'search', component: DummyComponent, pathMatch: 'full' }, - ]) - ], - declarations: [ - DummyComponent - ], - providers: [ - { provide: ItemDataService, useValue: {} }, - { provide: RouteService, useValue: {} }, - { provide: ResponseCacheService, useValue: {} }, - { provide: RequestService, useValue: {} }, - { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - { provide: RemoteDataBuildService, useValue: {} }, - { provide: GLOBAL_CONFIG, useValue: {} }, - SearchService - ], + describe('By default', () => { + let searchService: SearchService; + const router = new RouterStub(); + const route = new ActivatedRouteStub(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + RouterTestingModule.withRoutes([ + { path: 'search', component: DummyComponent, pathMatch: 'full' }, + ]) + ], + declarations: [ + DummyComponent + ], + providers: [ + { provide: ItemDataService, useValue: {} }, + { provide: RouteService, useValue: {} }, + { provide: ResponseCacheService, useValue: {} }, + { provide: RequestService, useValue: {} }, + { provide: ActivatedRoute, useValue: route }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: GLOBAL_CONFIG, useValue: {} }, + { provide: Router, useValue: router }, + SearchService + ], + }); + searchService = TestBed.get(SearchService); }); - searchService = TestBed.get(SearchService); - }); - it('should return list view mode by default', () => { - searchService.getViewMode().subscribe((viewMode) => { - expect(viewMode).toBe(ViewMode.List); + it('should return list view mode', () => { + searchService.getViewMode().subscribe((viewMode) => { + expect(viewMode).toBe(ViewMode.List); + }); }); }); + describe('', () => { + let searchService: SearchService; + const router = new RouterStub(); + const route = new ActivatedRouteStub(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + RouterTestingModule.withRoutes([ + { path: 'search', component: DummyComponent, pathMatch: 'full' }, + ]) + ], + declarations: [ + DummyComponent + ], + providers: [ + { provide: ItemDataService, useValue: {} }, + { provide: RouteService, useValue: {} }, + { provide: ResponseCacheService, useValue: {} }, + { provide: RequestService, useValue: {} }, + { provide: ActivatedRoute, useValue: route }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: GLOBAL_CONFIG, useValue: {} }, + { provide: Router, useValue: router }, + SearchService + ], + }); + searchService = TestBed.get(SearchService); + }); - it('should return the view mode set through setViewMode', fakeAsync(() => { - searchService.setViewMode(ViewMode.Grid) - tick(); - let viewMode = ViewMode.List; - searchService.getViewMode().subscribe((mode) => viewMode = mode); - expect(viewMode).toBe(ViewMode.Grid); + it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { + searchService.setViewMode(ViewMode.List); + expect(router.navigate).toHaveBeenCalledWith(['/search'], { + queryParams: { view: ViewMode.List }, + queryParamsHandling: 'merge' + }); + }); - searchService.setViewMode(ViewMode.List) - tick(); - searchService.getViewMode().subscribe((mode) => viewMode = mode); - expect(viewMode).toBe(ViewMode.List); - })); + it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { + searchService.setViewMode(ViewMode.Grid); + expect(router.navigate).toHaveBeenCalledWith(['/search'], { + queryParams: { view: ViewMode.Grid }, + queryParamsHandling: 'merge' + }); + }); + it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => { + let viewMode = ViewMode.Grid; + route.testParams = { view: ViewMode.List }; + searchService.getViewMode().subscribe((mode) => viewMode = mode); + expect(viewMode).toEqual(ViewMode.List); + }); + + it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => { + let viewMode = ViewMode.List; + route.testParams = { view: ViewMode.Grid }; + searchService.getViewMode().subscribe((mode) => viewMode = mode); + expect(viewMode).toEqual(ViewMode.Grid); + }); + }); }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 2dac0bd340..66e125bc58 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -88,13 +88,13 @@ export class SearchService extends HALEndpointService implements OnDestroy { // searchOptions: BehaviorSubject; searchOptions: SearchOptions; - constructor(protected responseCache: ResponseCacheService, + constructor(private router: Router, + private route: ActivatedRoute, + protected responseCache: ResponseCacheService, protected requestService: RequestService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private routeService: RouteService, - private route: ActivatedRoute, - private rdb: RemoteDataBuildService, - private router: Router) { + private rdb: RemoteDataBuildService,) { super(); const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; @@ -228,9 +228,10 @@ export class SearchService extends HALEndpointService implements OnDestroy { const value = searchFilterConfigName + ' ' + (i + 1); if (!selectedValues.includes(value)) { payload.push({ - value: value, - count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count - search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value)} + value: value, + count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count + search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value) + } ); } } @@ -271,12 +272,12 @@ export class SearchService extends HALEndpointService implements OnDestroy { getClearFiltersQueryParams(): any { const params = {}; this.sub = this.route.queryParamMap - .subscribe((map) => { - map.keys + .subscribe((pmap) => { + pmap.keys .filter((key) => this.config .findIndex((conf: SearchFilterConfig) => conf.paramName === key) < 0) .forEach((key) => { - params[key] = map.get(key); + params[key] = pmap.get(key); }) }); return params; diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index e9d200e15d..21df69b3a2 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -154,13 +154,6 @@ export class EndpointMapRequest extends GetRequest { } } -export class RootEndpointRequest extends EndpointMapRequest { - constructor(uuid: string, EnvConfig: GlobalConfig) { - const href = new RESTURLCombiner(EnvConfig, '/').toString(); - super(uuid, href); - } -} - export class BrowseEndpointRequest extends GetRequest { constructor(uuid: string, href: string) { super(uuid, href); diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 39652d6e7c..7c73e15fab 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -2,9 +2,9 @@ import { cold, hot } from 'jasmine-marbles'; import { GlobalConfig } from '../../../config/global-config.interface'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { RootEndpointRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from './hal-endpoint.service'; +import { EndpointMapRequest } from '../data/request.models'; describe('HALEndpointService', () => { let service: HALEndpointService; @@ -32,7 +32,7 @@ describe('HALEndpointService', () => { describe('getRootEndpointMap', () => { beforeEach(() => { responseCache = jasmine.createSpyObj('responseCache', { - get: hot('--a-', { + get: hot('a-', { a: { response: { endpointMap: endpointMap } } @@ -52,45 +52,51 @@ describe('HALEndpointService', () => { ); }); - it('should configure a new RootEndpointRequest', () => { + it('should configure a new EndpointMapRequest', () => { (service as any).getRootEndpointMap(); - const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig); + const expected = new EndpointMapRequest(requestService.generateRequestId(), envConfig.rest.baseUrl); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should return an Observable of the endpoint map', () => { const result = (service as any).getRootEndpointMap(); - const expected = cold('--b-', { b: endpointMap }); + const expected = cold('b-', { b: endpointMap }); expect(result).toBeObservable(expected); }); }); describe('getEndpoint', () => { + beforeEach(() => { + envConfig = { + rest: { baseUrl: 'https://rest.api/' } + } as any; + service = new TestService( responseCache, requestService, envConfig ); - - spyOn(service as any, 'getRootEndpointMap').and - .returnValue(hot('--a-', { a: endpointMap })); }); it('should return the endpoint URL for the service\'s linkPath', () => { + spyOn(service as any, 'getEndpointAt').and + .returnValue(hot('a-', { a: 'https://rest.api/test' })); const result = service.getEndpoint(); - const expected = cold('--b-', { b: endpointMap.test }); + + const expected = cold('b-', { b: endpointMap.test }); expect(result).toBeObservable(expected); }); it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { (service as any).linkPath = 'unknown'; + spyOn(service as any, 'getEndpointAt').and + .returnValue(hot('a-', { a: undefined })); const result = service.getEndpoint(); - const expected = cold('--b-', { b: undefined }); + const expected = cold('b-', { b: undefined }); expect(result).toBeObservable(expected); }); - }); describe('isEnabledOnRestApi', () => { @@ -127,7 +133,7 @@ describe('HALEndpointService', () => { (service as any).linkPath = 'unknown'; const result = service.isEnabledOnRestApi(); - const expected = cold('b-c-', { b: undefined, c: false }); + const expected = cold('b-c-', { b: undefined, c: false }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index c6c3ff284d..743179b23c 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -4,7 +4,7 @@ import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models'; -import { EndpointMapRequest, RootEndpointRequest } from '../data/request.models'; +import { EndpointMapRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts index 5d814ac7e5..7f8a3bb9fd 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts @@ -1,8 +1,6 @@ import { Component } from '@angular/core'; import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator'; - - import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { Collection } from '../../../../core/shared/collection.model'; import { ViewMode } from '../../../../+search-page/search-options.model'; diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts index f011e49eda..7c34207f5e 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts @@ -1,6 +1,4 @@ import { Component } from '@angular/core'; - - import { Community } from '../../../../core/shared/community.model'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; diff --git a/src/app/shared/testing/active-router-stub.ts b/src/app/shared/testing/active-router-stub.ts index 9959f38292..35c966d72a 100644 --- a/src/app/shared/testing/active-router-stub.ts +++ b/src/app/shared/testing/active-router-stub.ts @@ -1,16 +1,16 @@ -import { Params } from '@angular/router'; +import { convertToParamMap, ParamMap, Params } from '@angular/router'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; export class ActivatedRouteStub { private _testParams?: any; - // ActivatedRoute.params is Observable private subject?: BehaviorSubject = new BehaviorSubject(this.testParams); params = this.subject.asObservable(); queryParams = this.subject.asObservable(); + queryParamMap = this.subject.asObservable().map((params: Params) => convertToParamMap(params)); constructor(params?: Params) { if (params) { diff --git a/src/app/shared/testing/search-service-stub.ts b/src/app/shared/testing/search-service-stub.ts new file mode 100644 index 0000000000..23b2004827 --- /dev/null +++ b/src/app/shared/testing/search-service-stub.ts @@ -0,0 +1,32 @@ +import { Observable } from 'rxjs/Observable'; +import { ViewMode } from '../../+search-page/search-options.model'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +export class SearchServiceStub { + + private _viewMode: ViewMode; + private subject?: BehaviorSubject = new BehaviorSubject(this.testViewMode); + + viewMode = this.subject.asObservable(); + + constructor() { + this.setViewMode(ViewMode.List); + } + + getViewMode(): Observable { + return this.viewMode; + } + + setViewMode(viewMode: ViewMode) { + this.testViewMode = viewMode; + } + + get testViewMode(): ViewMode { + return this._viewMode; + } + + set testViewMode(viewMode: ViewMode) { + this._viewMode = viewMode; + this.subject.next(viewMode); + } +} diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index 3923fec0a6..1b4bf6da46 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -4,19 +4,12 @@ import { By } from '@angular/platform-browser'; import { MockTranslateLoader } from '../mocks/mock-translate-loader'; import { RouterTestingModule } from '@angular/router/testing'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { SearchService } from '../../+search-page/search-service/search.service'; -import { ItemDataService } from './../../core/data/item-data.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { ViewMode } from '../../+search-page/search-options.model'; -import { RouteService } from '../route.service'; -import { ResponseCacheService } from '../../core/cache/response-cache.service'; -import { RequestService } from '../../core/data/request.service'; -import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; -import { ActivatedRoute } from '@angular/router'; -import { GLOBAL_CONFIG } from '../../../config'; -import { ActivatedRouteStub } from '../testing/active-router-stub'; +import { SearchServiceStub } from '../testing/search-service-stub'; @Component({ template: '' }) class DummyComponent { } @@ -24,10 +17,9 @@ class DummyComponent { } describe('ViewModeSwitchComponent', () => { let comp: ViewModeSwitchComponent; let fixture: ComponentFixture; - let searchService: SearchService; + const searchService = new SearchServiceStub(); let listButton: HTMLElement; let gridButton: HTMLElement; - let route = new ActivatedRouteStub(); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -46,15 +38,10 @@ describe('ViewModeSwitchComponent', () => { DummyComponent ], providers: [ - { provide: ItemDataService, useValue: {} }, - { provide: RouteService, useValue: {} }, - { provide: ResponseCacheService, useValue: {} }, - { provide: RequestService, useValue: {} }, - { provide: ActivatedRoute, useValue: route }, - { provide: RemoteDataBuildService, useValue: {} }, - { provide: GLOBAL_CONFIG, useValue: {} }, - SearchService + { provide: SearchService, useValue: searchService }, ], + }).overrideComponent(ViewModeSwitchComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); })); @@ -65,12 +52,10 @@ describe('ViewModeSwitchComponent', () => { const debugElements = fixture.debugElement.queryAll(By.css('a')); listButton = debugElements[0].nativeElement; gridButton = debugElements[1].nativeElement; - searchService = fixture.debugElement.injector.get(SearchService); }); it('should set list button as active when on list mode', fakeAsync(() => { searchService.setViewMode(ViewMode.List); - route = new ActivatedRouteStub([{view: ViewMode.List}]) tick(); fixture.detectChanges(); expect(comp.currentMode).toBe(ViewMode.List); @@ -80,7 +65,6 @@ describe('ViewModeSwitchComponent', () => { it('should set grid button as active when on grid mode', fakeAsync(() => { searchService.setViewMode(ViewMode.Grid); - route = new ActivatedRouteStub([{view: ViewMode.Grid}]) tick(); fixture.detectChanges(); expect(comp.currentMode).toBe(ViewMode.Grid); From 3f36e0371e66d9db6f8c5c553edb351a4a6da9e4 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Mon, 19 Mar 2018 11:39:57 +0100 Subject: [PATCH 09/34] fixed e2e tests --- src/app/core/data/search-response-parsing.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 886da34200..583d63ec9d 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -6,7 +6,7 @@ import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { PageInfo } from '../shared/page-info.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; import { Metadatum } from '../shared/metadatum.model'; @@ -31,6 +31,7 @@ export class SearchResponseParsingService implements ResponseParsingService { }); const dsoSelfLinks = payload._embedded.objects + .filter((object) => hasValue(object._embedded)) .map((object) => object._embedded.dspaceObject) // we don't need embedded collections, bitstreamformats, etc for search results. // And parsing them all takes up a lot of time. Throw them away to improve performance @@ -44,6 +45,7 @@ export class SearchResponseParsingService implements ResponseParsingService { .reduce((combined, thisElement) => [...combined, ...thisElement], []); const objects = payload._embedded.objects + .filter((object) => hasValue(object._embedded)) .map((object, index) => Object.assign({}, object, { dspaceObject: dsoSelfLinks[index], hitHighlights: hitHighlights[index], From cdf1dc402aea093159891aa4a2165183cad5a2da Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Tue, 20 Mar 2018 11:39:51 +0100 Subject: [PATCH 10/34] 50463: use search/facet endpoint to retrieve facet values --- resources/i18n/en.json | 2 +- .../search-filter/search-filter.component.ts | 5 +- .../search-service/facet-value.model.ts | 8 +- .../search-service/search.service.ts | 149 ++++++++++++------ src/app/core/browse/browse.service.ts | 7 +- src/app/core/cache/response-cache.models.ts | 23 +++ src/app/core/config/config.service.ts | 13 +- .../submission-definitions-config.service.ts | 7 +- .../config/submission-forms-config.service.ts | 7 +- .../submission-sections-config.service.ts | 7 +- src/app/core/core.module.ts | 4 + src/app/core/data/collection-data.service.ts | 5 +- src/app/core/data/comcol-data.service.ts | 4 +- src/app/core/data/community-data.service.ts | 9 +- src/app/core/data/data.service.ts | 10 +- .../data/facet-response-parsing.service.ts | 41 +++++ src/app/core/data/item-data.service.ts | 8 +- src/app/core/shared/hal-endpoint.service.ts | 24 +-- 18 files changed, 236 insertions(+), 97 deletions(-) create mode 100644 src/app/core/data/facet-response-parsing.service.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 7b3d31c3e0..07f770825d 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -117,7 +117,7 @@ "placeholder": "Subject", "head": "Subject" }, - "date": { + "dateIssued": { "placeholder": "Date", "head": "Date" } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index ddb21236c5..b51a2d70fa 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -6,6 +6,7 @@ import { FacetValue } from '../../search-service/facet-value.model'; import { SearchFilterService } from './search-filter.service'; import { Observable } from 'rxjs/Observable'; import { slide } from '../../../shared/animations/slide'; +import { PaginatedList } from '../../../core/data/paginated-list'; /** * This component renders a simple item page. @@ -22,13 +23,13 @@ import { slide } from '../../../shared/animations/slide'; export class SearchFilterComponent implements OnInit { @Input() filter: SearchFilterConfig; - filterValues: Observable>; + filterValues: Observable>>; constructor(private searchService: SearchService, private filterService: SearchFilterService) { } ngOnInit() { - this.filterValues = this.searchService.getFacetValuesFor(this.filter.name); + this.filterValues = this.searchService.getFacetValuesFor(this.filter.name, '', ''); const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => { if (this.filter.isOpenByDefault || isActive) { this.initialExpand(); diff --git a/src/app/+search-page/search-service/facet-value.model.ts b/src/app/+search-page/search-service/facet-value.model.ts index a323970bc7..06eb50bc6b 100644 --- a/src/app/+search-page/search-service/facet-value.model.ts +++ b/src/app/+search-page/search-service/facet-value.model.ts @@ -1,7 +1,13 @@ +import { autoserialize, autoserializeAs } from 'cerialize'; + export class FacetValue { - + @autoserializeAs(String, 'label') value: string; + + @autoserialize count: number; + + @autoserialize search: string; } diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 66e125bc58..b20d65cf53 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,13 +1,14 @@ -import { Inject, Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { flatMap, map, tap } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; -import { SearchSuccessResponse } from '../../core/cache/response-cache.models'; +import { + FacetValueMapSuccessResponse, FacetValueSuccessResponse, + SearchSuccessResponse +} from '../../core/cache/response-cache.models'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -33,6 +34,7 @@ import { SearchQueryResponse } from './search-query-response.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { getSearchResultFor } from './search-result-element-decorator'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { FacetResponseParsingService } from '../../core/data/facet-response-parsing.service'; function shuffle(array: any[]) { let i = 0; @@ -49,20 +51,21 @@ function shuffle(array: any[]) { } @Injectable() -export class SearchService extends HALEndpointService implements OnDestroy { - protected linkPath = 'discover/search/objects'; +export class SearchService implements OnDestroy { + private searchLinkPath = 'discover/search/objects'; + private facetLinkPath = 'discover/search/facets'; private sub; uiSearchRoute = '/search'; config: SearchFilterConfig[] = [ - Object.assign(new SearchFilterConfig(), - { - name: 'scope', - type: FilterType.hierarchy, - hasFacets: true, - isOpenByDefault: true - }), + // Object.assign(new SearchFilterConfig(), + // { + // name: 'scope', + // type: FilterType.hierarchy, + // hasFacets: true, + // isOpenByDefault: true + // }), Object.assign(new SearchFilterConfig(), { name: 'author', @@ -72,7 +75,7 @@ export class SearchService extends HALEndpointService implements OnDestroy { }), Object.assign(new SearchFilterConfig(), { - name: 'date', + name: 'dateIssued', type: FilterType.range, hasFacets: true, isOpenByDefault: false @@ -92,10 +95,9 @@ export class SearchService extends HALEndpointService implements OnDestroy { private route: ActivatedRoute, protected responseCache: ResponseCacheService, protected requestService: RequestService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private routeService: RouteService, - private rdb: RemoteDataBuildService,) { - super(); + private rdb: RemoteDataBuildService, + private halService: HALEndpointService) { const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; @@ -106,7 +108,7 @@ export class SearchService extends HALEndpointService implements OnDestroy { } search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable> | PaginatedList>>> { - const requestObs = this.getEndpoint().pipe( + const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { const args: string[] = []; @@ -219,35 +221,92 @@ export class SearchService extends HALEndpointService implements OnDestroy { )); } - getFacetValuesFor(searchFilterConfigName: string): Observable> { - const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName); - return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => { - const payload: FacetValue[] = []; - const totalFilters = 13; - for (let i = 0; i < totalFilters; i++) { - const value = searchFilterConfigName + ' ' + (i + 1); - if (!selectedValues.includes(value)) { - payload.push({ - value: value, - count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count - search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value) - } - ); - } + getFacetValuesFor(searchFilterConfigName: string, query: string, scopeId: string): Observable>> { + const requestObs = this.halService.getEndpoint(this.facetLinkPath).pipe( + map((url: string) => { + const args: string[] = []; + + if (isNotEmpty(query)) { + args.push(`query=${query}`); } - const requestPending = false; - const responsePending = false; - const isSuccessful = true; - const error = undefined; - return new RemoteData( - requestPending, - responsePending, - isSuccessful, - error, - payload - ) + + if (isNotEmpty(scopeId)) { + args.push(`scope=${scopeId}`); + } + + if (isNotEmpty(args)) { + url = new URLCombiner(url, `?${args.join('&')}`).toString(); + } + + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return FacetResponseParsingService; + } + }); + }), + tap((request: RestRequest) => this.requestService.configure(request)), + ); + + const requestEntryObs = requestObs.pipe( + flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + + const responseCacheObs = requestObs.pipe( + flatMap((request: RestRequest) => this.responseCache.get(request.href)) + ); + + // get search results from response cache + const facetValueResponseObs: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: FacetValueMapSuccessResponse) => response.results[searchFilterConfigName]) + ); + + // get search results from response cache + const facetValueObs: Observable = facetValueResponseObs.pipe( + map((response: FacetValueSuccessResponse) => response.results) + ); + + const pageInfoObs: Observable = facetValueResponseObs.pipe( + map((response: FacetValueSuccessResponse) => { console.log(response); return response.pageInfo}) + ); + const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => { + if (hasValue(pageInfo)) { + return new PaginatedList(pageInfo, facetValue); + } else { + return facetValue; } - ) + }); + return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); + + // const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName); + // return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => { + // const payload: FacetValue[] = []; + // const totalFilters = 13; + // for (let i = 0; i < totalFilters; i++) { + // const value = searchFilterConfigName + ' ' + (i + 1); + // if (!selectedValues.includes(value)) { + // payload.push({ + // value: value, + // count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count + // search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value) + // } + // ); + // } + // } + // const requestPending = false; + // const responsePending = false; + // const isSuccessful = true; + // const error = undefined; + // return new RemoteData( + // requestPending, + // responsePending, + // isSuccessful, + // error, + // payload + // ) + // } + // ) } getViewMode(): Observable { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 3b283e2d93..a7b7314d54 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -12,7 +12,7 @@ import { BrowseDefinition } from '../shared/browse-definition.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() -export class BrowseService extends HALEndpointService { +export class BrowseService { protected linkPath = 'browses'; private static toSearchKeyArray(metadatumKey: string): string[] { @@ -31,13 +31,12 @@ export class BrowseService extends HALEndpointService { constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { - super(); + protected halService: HALEndpointService) { } getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); - return this.getEndpoint() + return this.halService.getEndpoint(linkPath) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)) diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index fb9f10ce51..5da328931b 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -3,6 +3,7 @@ import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { ConfigObject } from '../shared/config/config.model'; +import { FacetValue } from '../../+search-page/search-service/facet-value.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -32,6 +33,28 @@ export class SearchSuccessResponse extends RestResponse { } } +export class FacetValueMap { + [name: string]: FacetValueSuccessResponse +} + +export class FacetValueSuccessResponse extends RestResponse { + constructor( + public results: FacetValue[], + public statusCode: string, + public pageInfo?: PageInfo) { + super(true, statusCode); + } +} + +export class FacetValueMapSuccessResponse extends RestResponse { + constructor( + public results: FacetValueMap, + public statusCode: string, + ) { + super(true, statusCode); + } +} + export class EndpointMap { [linkPath: string]: string } diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index f1750bca84..bb863ad46f 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -1,10 +1,7 @@ -import { Injectable } from '@angular/core'; - import { Observable } from 'rxjs/Observable'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; @@ -12,13 +9,13 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigData } from './config-data'; -export abstract class ConfigService extends HALEndpointService { +export abstract class ConfigService { protected request: ConfigRequest; protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract linkPath: string; - protected abstract EnvConfig: GlobalConfig; protected abstract browseEndpoint: string; + protected abstract halService: HALEndpointService; protected getConfig(request: RestRequest): Observable { const [successResponse, errorResponse] = this.responseCache.get(request.href) @@ -68,7 +65,7 @@ export abstract class ConfigService extends HALEndpointService { } public getConfigAll(): Observable { - return this.getEndpoint() + return this.halService.getEndpoint(this.linkPath) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) @@ -85,7 +82,7 @@ export abstract class ConfigService extends HALEndpointService { } public getConfigByName(name: string): Observable { - return this.getEndpoint() + return this.halService.getEndpoint(this.linkPath) .map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() @@ -96,7 +93,7 @@ export abstract class ConfigService extends HALEndpointService { } public getConfigBySearch(options: FindAllOptions = {}): Observable { - return this.getEndpoint() + return this.halService.getEndpoint(this.linkPath) .map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts index 9655576e71..6cbe0c55b5 100644 --- a/src/app/core/config/submission-definitions-config.service.ts +++ b/src/app/core/config/submission-definitions-config.service.ts @@ -1,10 +1,9 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class SubmissionDefinitionsConfigService extends ConfigService { @@ -14,7 +13,7 @@ export class SubmissionDefinitionsConfigService extends ConfigService { constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + protected halService: HALEndpointService) { super(); } diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index 7209cd0fdf..27eac78218 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -1,10 +1,9 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class SubmissionFormsConfigService extends ConfigService { @@ -14,7 +13,7 @@ export class SubmissionFormsConfigService extends ConfigService { constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + protected halService: HALEndpointService) { super(); } diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts index 108fc30259..6d4d2ca825 100644 --- a/src/app/core/config/submission-sections-config.service.ts +++ b/src/app/core/config/submission-sections-config.service.ts @@ -1,10 +1,9 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; -import { GLOBAL_CONFIG } from '../../../config'; -import { GlobalConfig } from '../../../config/global-config.interface'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class SubmissionSectionsConfigService extends ConfigService { @@ -14,7 +13,7 @@ export class SubmissionSectionsConfigService extends ConfigService { constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + protected halService: HALEndpointService) { super(); } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 2960918ea7..764e21efba 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -40,6 +40,8 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { UUIDService } from './shared/uuid.service'; +import { HALEndpointService } from './shared/hal-endpoint.service'; +import { FacetResponseParsingService } from './data/facet-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -61,6 +63,7 @@ const PROVIDERS = [ CollectionDataService, DSOResponseParsingService, DSpaceRESTv2Service, + HALEndpointService, HostWindowService, ItemDataService, MetadataService, @@ -70,6 +73,7 @@ const PROVIDERS = [ RequestService, ResponseCacheService, EndpointMapResponseParsingService, + FacetResponseParsingService, DebugResponseParsingService, SearchResponseParsingService, ServerResponseService, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index a675b7254d..7d1e463dbe 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -10,6 +10,7 @@ import { Collection } from '../shared/collection.model'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -20,9 +21,9 @@ export class CollectionDataService extends ComColDataService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService ) { super(); } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 24c019c611..112afa0bc8 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -10,10 +10,12 @@ import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; import { FindByIDRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; + protected abstract halService: HALEndpointService; /** * Get the scoped endpoint URL by fetching the object with @@ -27,7 +29,7 @@ export abstract class ComColDataService { if (isEmpty(scopeID)) { - return this.getEndpoint(); + return this.halService.getEndpoint(this.linkPath); } else { const scopeCommunityHrefObs = this.cds.getEndpoint() .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 4cb44f4296..88ad3a5287 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -10,6 +10,7 @@ import { CoreState } from '../core.reducers'; import { Community } from '../shared/community.model'; import { ComColDataService } from './comcol-data.service'; import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -21,9 +22,13 @@ export class CommunityDataService extends ComColDataService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService ) { super(); } + + getEndpoint() { + return this.halService.getEndpoint(this.linkPath); + } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 1d9d568da3..f532ff05ba 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,11 +1,9 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; -import { GlobalConfig } from '../../../config'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; -import { GenericConstructor } from '../shared/generic-constructor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginatedList } from './paginated-list'; @@ -14,13 +12,13 @@ import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './r import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -export abstract class DataService extends HALEndpointService { +export abstract class DataService { protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; protected abstract linkPath: string; - protected abstract EnvConfig: GlobalConfig; + protected abstract halService: HALEndpointService; public abstract getScopedEndpoint(scope: string): Observable @@ -55,7 +53,7 @@ export abstract class DataService } findAll(options: FindAllOptions = {}): Observable>> { - const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href)) + const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href)) .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); hrefObs @@ -74,7 +72,7 @@ export abstract class DataService } findById(id: string): Observable> { - const hrefObs = this.getEndpoint() + const hrefObs = this.halService.getEndpoint(this.linkPath) .map((endpoint: string) => this.getFindByIDHref(endpoint, id)); hrefObs diff --git a/src/app/core/data/facet-response-parsing.service.ts b/src/app/core/data/facet-response-parsing.service.ts new file mode 100644 index 0000000000..8295d026a3 --- /dev/null +++ b/src/app/core/data/facet-response-parsing.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { + FacetValueMap, + FacetValueMapSuccessResponse, + FacetValueSuccessResponse, + RestResponse +} from '../cache/response-cache.models'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { PageInfo } from '../shared/page-info.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { FacetValue } from '../../+search-page/search-service/facet-value.model'; + +@Injectable() +export class FacetResponseParsingService implements ResponseParsingService { + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + + const payload = data.payload; + const facetMap: FacetValueMap = new FacetValueMap(); + + const serializer = new DSpaceRESTv2Serializer(FacetValue); + 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)); + facetMap[facet.name] = valuesResponse; + }); + + return new FacetValueMapSuccessResponse(facetMap, data.statusCode); + } + + protected processPageInfo(pageObj: any): PageInfo { + if (isNotEmpty(pageObj)) { + return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + } else { + return undefined; + } + } +} diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 0e6fcbfd33..6b0937d8e4 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -14,6 +14,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class ItemDataService extends DataService { @@ -24,15 +25,14 @@ export class ItemDataService extends DataService { protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private bs: BrowseService - ) { + private bs: BrowseService, + protected halService: HALEndpointService) { super(); } public getScopedEndpoint(scopeID: string): Observable { if (isEmpty(scopeID)) { - return this.getEndpoint(); + return this.halService.getEndpoint(this.linkPath); } else { return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath) .filter((href: string) => isNotEmpty(href)) diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 743179b23c..b9bb1d587c 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -8,13 +8,19 @@ import { EndpointMapRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; +import { Inject, Injectable } from '@angular/core'; +import { GLOBAL_CONFIG } from '../../../config'; -export abstract class HALEndpointService { - protected abstract responseCache: ResponseCacheService; - protected abstract requestService: RequestService; - protected abstract linkPath: string; - protected abstract EnvConfig: GlobalConfig; +@Injectable() +export class HALEndpointService { + protected linkPath: string; + + constructor(private responseCache: ResponseCacheService, + private requestService: RequestService, + @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { + + } protected getRootHref(): string { return new RESTURLCombiner(this.EnvConfig, '/').toString(); } @@ -33,8 +39,8 @@ export abstract class HALEndpointService { .distinctUntilChanged(); } - public getEndpoint(): Observable { - return this.getEndpointAt(...this.linkPath.split('/')); + public getEndpoint(linkPath: string): Observable { + return this.getEndpointAt(...linkPath.split('/')); } private getEndpointAt(...path: string[]): Observable { @@ -50,10 +56,10 @@ export abstract class HALEndpointService { return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); } - public isEnabledOnRestApi(): Observable { + public isEnabledOnRestApi(linkPath: string): Observable { return this.getRootEndpointMap().pipe( // TODO this only works when there's no / in linkPath - map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[this.linkPath])), + map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[linkPath])), startWith(undefined), distinctUntilChanged() ) From 86fcf44977601db5efc86b50de3af17ecc1374e5 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 29 Mar 2018 15:30:46 +0200 Subject: [PATCH 11/34] facet, filter, pagination implementation using rest api --- config/environment.default.js | 3 +- package.json | 1 + resources/i18n/en.json | 4 + .../paginated-search-options.model.ts | 20 ++ .../search-facet-filter.component.html | 29 +-- .../search-facet-filter.component.ts | 101 ++++++--- .../search-filter.component.html | 3 +- .../search-filter/search-filter.component.ts | 4 +- .../search-filter/search-filter.service.ts | 87 +++++++- .../search-filters.component.html | 2 +- src/app/+search-page/search-options.model.ts | 29 ++- src/app/+search-page/search-page.component.ts | 77 ++----- .../search-service/filter-type.model.ts | 5 +- .../search-filter-config.model.ts | 38 ++-- .../search-service/search.service.ts | 192 ++++++------------ .../search-settings.component.ts | 3 +- src/app/core/cache/response-cache.models.ts | 10 + src/app/core/core.effects.ts | 2 +- src/app/core/core.module.ts | 8 +- .../data/base-response-parsing.service.ts | 8 +- .../facet-config-response-parsing.service.ts | 32 +++ ...cet-value-map-response-parsing.service.ts} | 25 ++- .../facet-value-response-parsing.service.ts | 38 ++++ src/app/core/data/paginated-list.ts | 28 ++- .../data/search-response-parsing.service.ts | 10 +- src/app/core/shared/hal-endpoint.service.ts | 25 ++- src/app/shared/route.service.spec.ts | 14 +- src/app/shared/route.service.ts | 44 ++-- src/tsconfig.test.json | 2 +- yarn.lock | 10 + 30 files changed, 513 insertions(+), 341 deletions(-) create mode 100644 src/app/+search-page/paginated-search-options.model.ts create mode 100644 src/app/core/data/facet-config-response-parsing.service.ts rename src/app/core/data/{facet-response-parsing.service.ts => facet-value-map-response-parsing.service.ts} (68%) create mode 100644 src/app/core/data/facet-value-response-parsing.service.ts diff --git a/config/environment.default.js b/config/environment.default.js index ad7be69b9e..b44da0e248 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -18,7 +18,8 @@ module.exports = { // Caching settings cache: { // NOTE: how long should objects be cached for by default - msToLive: 15 * 60 * 1000, // 15 minute + msToLive: 15 * 60 * 1000, // 15 minutes + // msToLive: 1000, // 15 minutes control: 'max-age=60' // revalidate browser }, // Angular Universal settings diff --git a/package.json b/package.json index 710f62a257..ab875fd95e 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "@angular/compiler-cli": "^5.2.5", "@ngrx/store-devtools": "^5.1.0", "@ngtools/webpack": "^1.10.0", + "@types/acorn": "^4.0.3", "@types/cookie-parser": "1.4.1", "@types/deep-freeze": "0.1.1", "@types/express": "^4.11.1", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 07f770825d..68dfcbe147 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -120,6 +120,10 @@ "dateIssued": { "placeholder": "Date", "head": "Date" + }, + "has_content_in_original_bundle": { + "placeholder": "Has files", + "head": "Has files" } } } diff --git a/src/app/+search-page/paginated-search-options.model.ts b/src/app/+search-page/paginated-search-options.model.ts new file mode 100644 index 0000000000..0c403af827 --- /dev/null +++ b/src/app/+search-page/paginated-search-options.model.ts @@ -0,0 +1,20 @@ +import { SortOptions } from '../core/cache/models/sort-options.model'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { isNotEmpty } from '../shared/empty.util'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { SearchOptions } from './search-options.model'; + +export class PaginatedSearchOptions extends SearchOptions { + pagination?: PaginationComponentOptions; + sort?: SortOptions; + toRestUrl(url: string, args: string[] = []): string { + if (isNotEmpty(this.sort)) { + args.push(`sort=${this.sort.field},${this.sort.direction}`); + } + if (isNotEmpty(this.pagination)) { + args.push(`page=${this.pagination.currentPage - 1}`); + args.push(`size=${this.pagination.pageSize}`); + } + return super.toRestUrl(url, args); + } +} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html index 114837ce65..aa71c5c24c 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html @@ -2,26 +2,29 @@ diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 99501f346a..c7941ec5dc 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -1,10 +1,16 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FacetValue } from '../../../search-service/facet-value.model'; import { SearchFilterConfig } from '../../../search-service/search-filter-config.model'; -import { Params, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { SearchFilterService } from '../search-filter.service'; -import { isNotEmpty } from '../../../../shared/empty.util'; +import { 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'; +import { SearchOptions } from '../../../search-options.model'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Subscription } from 'rxjs/Subscription'; /** * This component renders a simple item page. @@ -15,21 +21,43 @@ import { isNotEmpty } from '../../../../shared/empty.util'; @Component({ selector: 'ds-search-facet-filter', styleUrls: ['./search-facet-filter.component.scss'], - templateUrl: './search-facet-filter.component.html', + templateUrl: './search-facet-filter.component.html' }) -export class SearchFacetFilterComponent implements OnInit { - @Input() filterValues: FacetValue[]; +export class SearchFacetFilterComponent implements OnInit, OnDestroy { @Input() filterConfig: SearchFilterConfig; @Input() selectedValues: string[]; + filterValues: Array>>> = []; + filterValues$: BehaviorSubject = new BehaviorSubject(this.filterValues); currentPage: Observable; filter: string; + pageChange = false; + sub: Subscription; - constructor(private filterService: SearchFilterService, private router: Router) { + constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) { } ngOnInit(): void { - this.currentPage = this.filterService.getPage(this.filterConfig.name); + this.currentPage = this.getCurrentPage(); + this.currentPage.distinctUntilChanged().subscribe((page) => this.pageChange = true); + this.filterService.getSearchOptions().distinctUntilChanged().subscribe((options) => this.updateFilterValueList(options)); + } + + updateFilterValueList(options: SearchOptions) { + if (!this.pageChange) { + this.showFirstPageOnly(); + } + 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); + }); + // this.filterValues.subscribe((c) => c.map((a) => a.subscribe((b) => console.log(b)))); } isChecked(value: FacetValue): Observable { @@ -37,23 +65,7 @@ export class SearchFacetFilterComponent implements OnInit { } getSearchLink() { - return this.filterService.searchLink; - } - - getQueryParamsWith(value: string): Observable { - return this.filterService.getQueryParamsWith(this.filterConfig, value); - } - - getQueryParamsWithout(value: string): Observable { - return this.filterService.getQueryParamsWithout(this.filterConfig, value); - } - - get facetCount(): Observable { - const resultCount = this.filterValues.length; - return this.currentPage.map((page: number) => { - const max = page * this.filterConfig.pageSize; - return max > resultCount ? resultCount : max; - }); + return this.searchService.getSearchLink(); } showMore() { @@ -61,6 +73,7 @@ export class SearchFacetFilterComponent implements OnInit { } showFirstPageOnly() { + this.filterValues = []; this.filterService.resetPage(this.filterConfig.name); } @@ -74,13 +87,39 @@ export class SearchFacetFilterComponent implements OnInit { onSubmit(data: any) { if (isNotEmpty(data)) { - const sub = this.getQueryParamsWith(data[this.filterConfig.paramName]).first().subscribe((params) => { - this.router.navigate([this.getSearchLink()], { queryParams: params } - ); - } - ); + this.router.navigate([this.getSearchLink()], { + queryParams: + { [this.filterConfig.paramName]: [...this.selectedValues, data[this.filterConfig.paramName]] }, + queryParamsHandling: 'merge' + }); this.filter = ''; - sub.unsubscribe(); + } + } + + hasValue(o: any): boolean { + return hasValue(o); + } + + isLastPage(): Observable { + return Observable.of(false); + // return this.filterValues.flatMap((map) => map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages)); + } + + getRemoveParams(value: string) { + return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) }; + } + + getAddParams(value: string) { + return { [this.filterConfig.paramName]: [...this.selectedValues, value] }; + } + + ngOnDestroy(): void { + this.unsubscribe(); + } + + unsubscribe(): void { + if (this.sub !== undefined) { + this.sub.unsubscribe(); } } } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-filter.component.html index f5acb42b6d..6cf9df9b05 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.html @@ -2,7 +2,6 @@
{{'search.filters.filter.' + filter.name + '.head'| translate}}
- +
\ No newline at end of file diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index b51a2d70fa..90d3b50786 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -23,13 +23,11 @@ import { PaginatedList } from '../../../core/data/paginated-list'; export class SearchFilterComponent implements OnInit { @Input() filter: SearchFilterConfig; - filterValues: Observable>>; - constructor(private searchService: SearchService, private filterService: SearchFilterService) { + constructor(private filterService: SearchFilterService) { } ngOnInit() { - this.filterValues = this.searchService.getFacetValuesFor(this.filter.name, '', ''); const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => { if (this.filter.isOpenByDefault || isActive) { this.initialExpand(); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 8a909b6fa7..9c5e406a78 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -14,6 +14,11 @@ import { hasValue, } from '../../../shared/empty.util'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchService } from '../../search-service/search.service'; import { RouteService } from '../../../shared/route.service'; +import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SearchOptions } from '../../search-options.model'; +import { PaginatedSearchOptions } from '../../paginated-search-options.model'; const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; @@ -21,8 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; export class SearchFilterService { constructor(private store: Store, - private routeService: RouteService, - private searchService: SearchService) { + private routeService: RouteService) { } isFilterActiveWithValue(paramName: string, filterValue: string): Observable { @@ -33,22 +37,85 @@ export class SearchFilterService { return this.routeService.hasQueryParam(paramName); } - getQueryParamsWithout(filterConfig: SearchFilterConfig, value: string) { - return this.routeService.removeQueryParameterValue(filterConfig.paramName, value); + getCurrentScope() { + return this.routeService.getQueryParameterValue('scope'); } - getQueryParamsWith(filterConfig: SearchFilterConfig, value: string) { - return this.routeService.addQueryParameterValue(filterConfig.paramName, value); + getCurrentQuery() { + return this.routeService.getQueryParameterValue('query'); + } + + getCurrentPagination(pagination: any = {}): Observable { + const page$ = this.routeService.getQueryParameterValue('page'); + const size$ = this.routeService.getQueryParameterValue('pageSize'); + return Observable.combineLatest(page$, size$, (page, size) => { + return Object.assign(new PaginationComponentOptions(), pagination, { + currentPage: page || 1, + pageSize: size || pagination.pageSize + }); + }); + } + + getCurrentSort(): Observable { + const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); + const sortField$ = this.routeService.getQueryParameterValue('sortField'); + return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => new SortOptions(sortField || undefined, SortDirection[sortDirection])); + } + + getCurrentFilters() { + return this.routeService.getQueryParamsWithPrefix('f.'); + } + + getCurrentView() { + return this.routeService.getQueryParameterValue('view'); + } + + getPaginatedSearchOptions(defaults: any = {}): Observable { + return Observable.combineLatest( + this.getCurrentPagination(defaults.pagination), + this.getCurrentSort(), + this.getCurrentView(), + this.getCurrentScope(), + this.getCurrentQuery(), + this.getCurrentFilters(), + (pagination, sort, view, scope, query, filters) => { + return Object.assign(new SearchOptions(), + defaults, + { + pagination: pagination, + sort: sort, + view: view, + scope: scope, + query: query, + filters: filters + }) + } + ) + } + + getSearchOptions(defaults: any = {}): Observable { + return Observable.combineLatest( + this.getCurrentView(), + this.getCurrentScope(), + this.getCurrentQuery(), + this.getCurrentFilters(), + (view, scope, query, filters) => { + return Object.assign(new SearchOptions(), + defaults, + { + view: view, + scope: scope, + query: query, + filters: filters + }) + } + ) } getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable { return this.routeService.getQueryParameterValues(filterConfig.paramName); } - get searchLink() { - return this.searchService.uiSearchRoute; - } - isCollapsed(filterName: string): Observable { return this.store.select(filterByNameSelector(filterName)) .map((object: SearchFilterState) => { diff --git a/src/app/+search-page/search-filters/search-filters.component.html b/src/app/+search-page/search-filters/search-filters.component.html index 7f375b1238..09782b68d4 100644 --- a/src/app/+search-page/search-filters/search-filters.component.html +++ b/src/app/+search-page/search-filters/search-filters.component.html @@ -1,5 +1,5 @@

{{"search.filters.head" | translate}}

-
+
diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts index 7f93c3ace1..4164321680 100644 --- a/src/app/+search-page/search-options.model.ts +++ b/src/app/+search-page/search-options.model.ts @@ -1,5 +1,5 @@ -import { SortOptions } from '../core/cache/models/sort-options.model'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { isNotEmpty } from '../shared/empty.util'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; export enum ViewMode { List = 'list', @@ -7,7 +7,28 @@ export enum ViewMode { } export class SearchOptions { - pagination?: PaginationComponentOptions; - sort?: SortOptions; view?: ViewMode = ViewMode.List; + scope?: string; + query?: string; + filters?: any; + + toRestUrl(url: string, args: string[] = []): string { + + if (isNotEmpty(this.query)) { + args.push(`query=${this.query}`); + } + + if (isNotEmpty(this.scope)) { + args.push(`scope=${this.scope}`); + } + if (isNotEmpty(this.filters)) { + Object.entries(this.filters).forEach(([key, values]) => { + values.forEach((value) => args.push(`${key}=${value},equals`)); + }); + } + if (isNotEmpty(args)) { + url = new URLCombiner(url, `?${args.join('&')}`).toString(); + } + return url; + } } diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 9c5c9a7462..57fb02b730 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -15,6 +15,7 @@ import { SearchOptions, ViewMode } from './search-options.model'; import { SearchResult } from './search-result.model'; import { SearchService } from './search-service/search.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; +import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; /** * This component renders a simple item page. @@ -36,85 +37,43 @@ export class SearchPageComponent implements OnInit, OnDestroy { query: string; scopeObjectRDObs: Observable>; - resultsRDObs: Observable> | PaginatedList>>>; + resultsRDObs: Observable>>>; currentParams = {}; searchOptions: SearchOptions; sortConfig: SortOptions; scopeListRDObs: Observable>>; isMobileView: Observable; + pageSize; + pageSizeOptions; + defaults = { + pagination: { + id: 'search-results-pagination', + pageSize: 10 + }, + query: '' + }; constructor(private service: SearchService, - private route: ActivatedRoute, private communityService: CommunityDataService, private sidebarService: SearchSidebarService, - private windowService: HostWindowService) { - this.isMobileView = Observable.combineLatest( + private windowService: HostWindowService, + private filterService: SearchFilterService) { + this.isMobileView = Observable.combineLatest( this.windowService.isXs(), this.windowService.isSm(), ((isXs, isSm) => isXs || isSm) ); this.scopeListRDObs = communityService.findAll(); - // Initial pagination config - const pagination: PaginationComponentOptions = new PaginationComponentOptions(); - pagination.id = 'search-results-pagination'; - pagination.currentPage = 1; - pagination.pageSize = 10; - - const sort: SortOptions = new SortOptions(); - this.sortConfig = sort; - this.searchOptions = this.service.searchOptions; } ngOnInit(): void { - this.sub = this.route - .queryParams - .subscribe((params) => { - // Save current parameters - this.currentParams = params; - this.query = params.query || ''; - this.scope = params.scope; - const page = +params.page || this.searchOptions.pagination.currentPage; - let pageSize = +params.pageSize || this.searchOptions.pagination.pageSize; - let pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100]; - - if (isNotEmpty(params.view) && params.view === ViewMode.Grid) { - pageSizeOptions = [12, 24, 36, 48 , 50, 62, 74, 84]; - if (pageSizeOptions.indexOf(pageSize) === -1) { - pageSize = 12; - } - } - if (isNotEmpty(params.view) && params.view === ViewMode.List) { - if (pageSizeOptions.indexOf(pageSize) === -1) { - pageSize = 10; - } - } - - const sortDirection = params.sortDirection || this.searchOptions.sort.direction; - const sortField = params.sortField || this.searchOptions.sort.field; - const pagination = Object.assign({}, - this.searchOptions.pagination, - { currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions} - ); - const sort = Object.assign({}, - this.searchOptions.sort, - { direction: sortDirection, field: sortField } - ); - - this.updateSearchResults({ - pagination: pagination, - sort: sort - }); - if (isNotEmpty(this.scope)) { - this.scopeObjectRDObs = this.communityService.findById(this.scope); - } else { - this.scopeObjectRDObs = Observable.of(undefined); - } - } - ); + this.sub = this.filterService.getPaginatedSearchOptions(this.defaults).subscribe((options) => { + this.updateSearchResults(options); + }); } private updateSearchResults(searchOptions) { - this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions); + this.resultsRDObs = this.service.search(searchOptions); this.searchOptions = searchOptions; } diff --git a/src/app/+search-page/search-service/filter-type.model.ts b/src/app/+search-page/search-service/filter-type.model.ts index fba0edfac4..354ca87f98 100644 --- a/src/app/+search-page/search-service/filter-type.model.ts +++ b/src/app/+search-page/search-service/filter-type.model.ts @@ -1,5 +1,6 @@ export enum FilterType { text, - range, - hierarchy + date, + hierarchical, + standard } diff --git a/src/app/+search-page/search-service/search-filter-config.model.ts b/src/app/+search-page/search-service/search-filter-config.model.ts index 1464300daa..2b77ef6768 100644 --- a/src/app/+search-page/search-service/search-filter-config.model.ts +++ b/src/app/+search-page/search-service/search-filter-config.model.ts @@ -1,17 +1,27 @@ -import { FilterType } from './filter-type.model'; + import { FilterType } from './filter-type.model'; + import { autoserialize, autoserializeAs } from 'cerialize'; -export class SearchFilterConfig { + export class SearchFilterConfig { - name: string; - type: FilterType; - hasFacets: boolean; - pageSize = 5; - isOpenByDefault: boolean; - /** - * Name of this configuration that can be used in a url - * @returns Parameter name - */ - get paramName(): string { - return 'f.' + this.name; + @autoserialize + name: string; + + @autoserializeAs(String, 'facetType') + type: FilterType; + + @autoserialize + hasFacets: boolean; + + // @autoserializeAs(String, 'facetLimit') - uncomment when fixed in rest + pageSize = 5; + + @autoserialize + isOpenByDefault: boolean; + /** + * Name of this configuration that can be used in a url + * @returns Parameter name + */ + get paramName(): string { + return 'f.' + this.name; + } } -} diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index b20d65cf53..2ef69e8578 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -6,7 +6,8 @@ import { ViewMode } from '../../+search-page/search-options.model'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { - FacetValueMapSuccessResponse, FacetValueSuccessResponse, + FacetConfigSuccessResponse, + FacetValueSuccessResponse, SearchSuccessResponse } from '../../core/cache/response-cache.models'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; @@ -34,26 +35,17 @@ import { SearchQueryResponse } from './search-query-response.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { getSearchResultFor } from './search-result-element-decorator'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { FacetResponseParsingService } from '../../core/data/facet-response-parsing.service'; - -function shuffle(array: any[]) { - let i = 0; - let j = 0; - let temp = null; - - for (i = array.length - 1; i > 0; i -= 1) { - j = Math.floor(Math.random() * (i + 1)); - temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - return array; -} +import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service'; +import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service'; +import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; +import { PaginatedSearchOptions } from '../paginated-search-options.model'; @Injectable() export class SearchService implements OnDestroy { private searchLinkPath = 'discover/search/objects'; - private facetLinkPath = 'discover/search/facets'; + private facetValueLinkPath = 'discover/search/facets'; + private facetValueLinkPathPrefix = 'discover/facets/'; + private facetConfigLinkPath = 'discover/facets'; private sub; uiSearchRoute = '/search'; @@ -62,7 +54,7 @@ export class SearchService implements OnDestroy { // Object.assign(new SearchFilterConfig(), // { // name: 'scope', - // type: FilterType.hierarchy, + // type: FilterType.hierarchical, // hasFacets: true, // isOpenByDefault: true // }), @@ -76,7 +68,7 @@ export class SearchService implements OnDestroy { Object.assign(new SearchFilterConfig(), { name: 'dateIssued', - type: FilterType.range, + type: FilterType.date, hasFacets: true, isOpenByDefault: false }), @@ -95,7 +87,6 @@ export class SearchService implements OnDestroy { private route: ActivatedRoute, protected responseCache: ResponseCacheService, protected requestService: RequestService, - private routeService: RouteService, private rdb: RemoteDataBuildService, private halService: HALEndpointService) { const pagination: PaginationComponentOptions = new PaginationComponentOptions(); @@ -103,36 +94,15 @@ export class SearchService implements OnDestroy { pagination.currentPage = 1; pagination.pageSize = 10; const sort: SortOptions = new SortOptions(); - this.searchOptions = { pagination: pagination, sort: sort }; - // this.searchOptions = new BehaviorSubject(searchOptions); + this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort }); } - search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable> | PaginatedList>>> { + search(searchOptions?: PaginatedSearchOptions): Observable>>> { const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { - const args: string[] = []; - - if (isNotEmpty(query)) { - args.push(`query=${query}`); + if (hasValue(searchOptions)) { + url = searchOptions.toRestUrl(url); } - - if (isNotEmpty(scopeId)) { - args.push(`scope=${scopeId}`); - } - - if (isNotEmpty(searchOptions)) { - if (isNotEmpty(searchOptions.sort)) { - args.push(`sort=${searchOptions.sort.field},${searchOptions.sort.direction}`); - } - if (isNotEmpty(searchOptions.pagination)) { - args.push(`page=${searchOptions.pagination.currentPage - 1}`); - args.push(`size=${searchOptions.pagination.pageSize}`); - } - } - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); return Object.assign(request, { getResponseParser(): GenericConstructor { @@ -183,55 +153,25 @@ export class SearchService implements OnDestroy { }); }); - const pageInfoObs: Observable = responseCacheObs - .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => { - if (hasValue((entry.response as SearchSuccessResponse).pageInfo)) { - const resPageInfo = (entry.response as SearchSuccessResponse).pageInfo; - if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { - return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); - } else { - return resPageInfo; - } - } - }); + const pageInfoObs: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: FacetValueSuccessResponse) => response.pageInfo) + ); const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => { - if (hasValue(pageInfo)) { - return new PaginatedList(pageInfo, tDomainList); - } else { - return tDomainList; - } + return new PaginatedList(pageInfo, tDomainList); }); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } - getConfig(): Observable> { - const requestPending = false; - const responsePending = false; - const isSuccessful = true; - const error = undefined; - return Observable.of(new RemoteData( - requestPending, - responsePending, - isSuccessful, - error, - this.config - )); - } - - getFacetValuesFor(searchFilterConfigName: string, query: string, scopeId: string): Observable>> { - const requestObs = this.halService.getEndpoint(this.facetLinkPath).pipe( + getConfig(scope?: string): Observable> { + const requestObs = this.halService.getEndpoint(this.facetConfigLinkPath).pipe( map((url: string) => { const args: string[] = []; - if (isNotEmpty(query)) { - args.push(`query=${query}`); - } - - if (isNotEmpty(scopeId)) { - args.push(`scope=${scopeId}`); + if (isNotEmpty(scope)) { + args.push(`scope=${scope}`); } if (isNotEmpty(args)) { @@ -241,7 +181,7 @@ export class SearchService implements OnDestroy { const request = new GetRequest(this.requestService.generateRequestId(), url); return Object.assign(request, { getResponseParser(): GenericConstructor { - return FacetResponseParsingService; + return FacetConfigResponseParsingService; } }); }), @@ -257,56 +197,56 @@ export class SearchService implements OnDestroy { ); // get search results from response cache - const facetValueResponseObs: Observable = responseCacheObs.pipe( + const facetConfigObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), - map((response: FacetValueMapSuccessResponse) => response.results[searchFilterConfigName]) + map((response: FacetConfigSuccessResponse) => response.results) + ); + + return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); + } + + getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable>> { + console.log('facetvalues'); + const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe( + map((url: string) => { + const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`]; + if (hasValue(searchOptions)) { + url = searchOptions.toRestUrl(url, args); + } + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return FacetValueResponseParsingService; + } + }); + }), + tap((request: RestRequest) => this.requestService.configure(request)), + ); + + const requestEntryObs = requestObs.pipe( + flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + + const responseCacheObs = requestObs.pipe( + flatMap((request: RestRequest) => this.responseCache.get(request.href)) ); // get search results from response cache - const facetValueObs: Observable = facetValueResponseObs.pipe( + const facetValueObs: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), map((response: FacetValueSuccessResponse) => response.results) ); - const pageInfoObs: Observable = facetValueResponseObs.pipe( - map((response: FacetValueSuccessResponse) => { console.log(response); return response.pageInfo}) + const pageInfoObs: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: FacetValueSuccessResponse) => response.pageInfo) ); - const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => { - if (hasValue(pageInfo)) { - return new PaginatedList(pageInfo, facetValue); - } else { - return facetValue; - } - }); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); - // const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName); - // return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => { - // const payload: FacetValue[] = []; - // const totalFilters = 13; - // for (let i = 0; i < totalFilters; i++) { - // const value = searchFilterConfigName + ' ' + (i + 1); - // if (!selectedValues.includes(value)) { - // payload.push({ - // value: value, - // count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count - // search: (decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value) - // } - // ); - // } - // } - // const requestPending = false; - // const responsePending = false; - // const isSuccessful = true; - // const error = undefined; - // return new RemoteData( - // requestPending, - // responsePending, - // isSuccessful, - // error, - // payload - // ) - // } - // ) + const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => { + return new PaginatedList(pageInfo, facetValue); + }); + + return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } getViewMode(): Observable { diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index bc1fb096fd..cc22da7176 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -3,6 +3,7 @@ import { SearchService } from '../search-service/search.service'; import { SearchOptions, ViewMode } from '../search-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { PaginatedSearchOptions } from '../paginated-search-options.model'; @Component({ selector: 'ds-search-settings', @@ -11,7 +12,7 @@ import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; }) export class SearchSettingsComponent implements OnInit { - @Input() searchOptions: SearchOptions; + @Input() searchOptions: PaginatedSearchOptions; /** * Declare SortDirection enumeration to use it in the template */ diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 5da328931b..f061e78e6c 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -4,6 +4,7 @@ import { PageInfo } from '../shared/page-info.model'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { ConfigObject } from '../shared/config/config.model'; import { FacetValue } from '../../+search-page/search-service/facet-value.model'; +import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -33,6 +34,15 @@ export class SearchSuccessResponse extends RestResponse { } } +export class FacetConfigSuccessResponse extends RestResponse { + constructor( + public results: SearchFilterConfig[], + public statusCode: string + ) { + super(true, statusCode); + } +} + export class FacetValueMap { [name: string]: FacetValueSuccessResponse } diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 7cda10b4ae..ebb87bf1ee 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -8,5 +8,5 @@ export const coreEffects = [ ResponseCacheEffects, RequestEffects, ObjectCacheEffects, - UUIDIndexEffects, + UUIDIndexEffects ]; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 764e21efba..86abf87d62 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -41,7 +41,9 @@ import { SubmissionFormsConfigService } from './config/submission-forms-config.s import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { UUIDService } from './shared/uuid.service'; import { HALEndpointService } from './shared/hal-endpoint.service'; -import { FacetResponseParsingService } from './data/facet-response-parsing.service'; +import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; +import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; +import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -73,7 +75,9 @@ const PROVIDERS = [ RequestService, ResponseCacheService, EndpointMapResponseParsingService, - FacetResponseParsingService, + FacetValueResponseParsingService, + FacetValueMapResponseParsingService, + FacetConfigResponseParsingService, DebugResponseParsingService, SearchResponseParsingService, ServerResponseService, diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index d8a4221420..9d6a5851e5 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -117,9 +117,13 @@ export abstract class BaseResponseParsingService { this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref); } - protected processPageInfo(pageObj: any): PageInfo { + processPageInfo(pageObj: any): PageInfo { if (isNotEmpty(pageObj)) { - return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + if (pageInfoObject.currentPage >= 0) { + Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 }); + } + return pageInfoObject } else { return undefined; } diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts new file mode 100644 index 0000000000..b0d89fb03e --- /dev/null +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable } from '@angular/core'; +import { + FacetConfigSuccessResponse, + RestResponse +} from '../cache/response-cache.models'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../config'; + +@Injectable() +export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + objectFactory = {}; + toCache = false; + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + + const config = data.payload._embedded.facets; + const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig); + const facetConfig = serializer.deserializeArray(config); + return new FacetConfigSuccessResponse(facetConfig, data.statusCode); + } +} diff --git a/src/app/core/data/facet-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts similarity index 68% rename from src/app/core/data/facet-response-parsing.service.ts rename to src/app/core/data/facet-value-map-response-parsing.service.ts index 8295d026a3..dfd72c0cc5 100644 --- a/src/app/core/data/facet-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { FacetValueMap, FacetValueMapSuccessResponse, @@ -12,9 +12,22 @@ import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.seriali import { PageInfo } from '../shared/page-info.model'; import { isNotEmpty } from '../../shared/empty.util'; import { FacetValue } from '../../+search-page/search-service/facet-value.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../config'; @Injectable() -export class FacetResponseParsingService implements ResponseParsingService { +export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + objectFactory = {}; + toCache = false; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; @@ -30,12 +43,4 @@ export class FacetResponseParsingService implements ResponseParsingService { return new FacetValueMapSuccessResponse(facetMap, data.statusCode); } - - protected processPageInfo(pageObj: any): PageInfo { - if (isNotEmpty(pageObj)) { - return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); - } else { - return undefined; - } - } } diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts new file mode 100644 index 0000000000..17f0730566 --- /dev/null +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@angular/core'; +import { + FacetValueMap, + FacetValueMapSuccessResponse, + FacetValueSuccessResponse, + RestResponse +} from '../cache/response-cache.models'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { PageInfo } from '../shared/page-info.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { FacetValue } from '../../+search-page/search-service/facet-value.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; + +@Injectable() +export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + objectFactory = {}; + toCache = false; + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + 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 facetValues = serializer.deserializeArray(values); + return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page)); + } +} diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index f1d076927d..7e4a57f84e 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -1,15 +1,17 @@ import { PageInfo } from '../shared/page-info.model'; +import { hasValue } from '../../shared/empty.util'; export class PaginatedList { - constructor( - private pageInfo: PageInfo, - public page: T[] - ) { + constructor(private pageInfo: PageInfo, + public page: T[]) { } get elementsPerPage(): number { - return this.pageInfo.elementsPerPage; + if (hasValue(this.pageInfo)) { + return this.pageInfo.elementsPerPage; + } + return this.page.length; } set elementsPerPage(value: number) { @@ -17,7 +19,10 @@ export class PaginatedList { } get totalElements(): number { - return this.pageInfo.totalElements; + if (hasValue(this.pageInfo)) { + return this.pageInfo.totalElements; + } + return this.page.length; } set totalElements(value: number) { @@ -25,7 +30,10 @@ export class PaginatedList { } get totalPages(): number { - return this.pageInfo.totalPages; + if (hasValue(this.pageInfo)) { + return this.pageInfo.totalPages; + } + return 1; } set totalPages(value: number) { @@ -33,7 +41,11 @@ export class PaginatedList { } get currentPage(): number { - return this.pageInfo.currentPage; + if (hasValue(this.pageInfo)) { + return this.pageInfo.currentPage; + } + return 1; + } set currentPage(value: number) { diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 583d63ec9d..927c5ff010 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -56,14 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService { })); payload.objects = objects; const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload); - return new SearchSuccessResponse(deserialized, data.statusCode, this.processPageInfo(data.payload.page)); - } - - protected processPageInfo(pageObj: any): PageInfo { - if (isNotEmpty(pageObj)) { - return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); - } else { - return undefined; - } + return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page)); } } diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index b9bb1d587c..c81c3c792d 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,12 +1,12 @@ import { Observable } from 'rxjs/Observable'; -import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators'; +import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models'; import { EndpointMapRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; @@ -21,6 +21,7 @@ export class HALEndpointService { @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { } + protected getRootHref(): string { return new RESTURLCombiner(this.EnvConfig, '/').toString(); } @@ -34,23 +35,35 @@ export class HALEndpointService { this.requestService.configure(request); return this.responseCache.get(request.href) .map((entry: ResponseCacheEntry) => entry.response) - .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) + .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response)) .map((response: EndpointMapSuccessResponse) => response.endpointMap) .distinctUntilChanged(); } public getEndpoint(linkPath: string): Observable { - return this.getEndpointAt(...linkPath.split('/')); + const test = this.getEndpointAt(...linkPath.split('/')); + // test.subscribe((test) => console.log(linkPath, test)); + return test; } private getEndpointAt(...path: string[]): Observable { if (isEmpty(path)) { path = ['/']; } + let currentPath; const pipeArguments = path - .map((subPath: string) => [ + .map((subPath: string, index: number) => [ flatMap((href: string) => this.getEndpointMapAt(href)), - map((endpointMap: EndpointMap) => endpointMap[subPath]), + map((endpointMap: EndpointMap) => { + if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) { + currentPath = endpointMap[subPath]; + return endpointMap[subPath]; + } else { + /*TODO remove if/else block once the rest response contains _links for facets*/ + currentPath += '/' + subPath; + return currentPath; + } + }), ]) .reduce((combined, thisElement) => [...combined, ...thisElement], []); return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); diff --git a/src/app/shared/route.service.spec.ts b/src/app/shared/route.service.spec.ts index 10bd147e1d..00b7d78be6 100644 --- a/src/app/shared/route.service.spec.ts +++ b/src/app/shared/route.service.spec.ts @@ -69,12 +69,12 @@ describe('RouteService', () => { describe('addQueryParameterValue', () => { it('should return a list of values that contains the added value when a new value is added and the parameter did not exist yet', () => { - service.addQueryParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => { + service.resolveRouteWithParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => { expect(params[nonExistingParamName]).toContain(nonExistingParamValue); }); }); it('should return a list of values that contains the existing values and the added value when a new value is added and the parameter already has values', () => { - service.addQueryParameterValue(paramName1, nonExistingParamValue).subscribe((params) => { + service.resolveRouteWithParameterValue(paramName1, nonExistingParamValue).subscribe((params) => { const values = params[paramName1]; expect(values).toContain(paramValue1); expect(values).toContain(nonExistingParamValue); @@ -84,7 +84,7 @@ describe('RouteService', () => { describe('removeQueryParameterValue', () => { it('should return a list of values that does not contain the removed value when the parameter value exists', () => { - service.removeQueryParameterValue(paramName2, paramValue2a).subscribe((params) => { + service.resolveRouteWithoutParameterValue(paramName2, paramValue2a).subscribe((params) => { const values = params[paramName2]; expect(values).toContain(paramValue2b); expect(values).not.toContain(paramValue2a); @@ -92,7 +92,7 @@ describe('RouteService', () => { }); it('should return a list of values that does contain all existing values when the removed parameter does not exist', () => { - service.removeQueryParameterValue(paramName2, nonExistingParamValue).subscribe((params) => { + service.resolveRouteWithoutParameterValue(paramName2, nonExistingParamValue).subscribe((params) => { const values = params[paramName2]; expect(values).toContain(paramValue2a); expect(values).toContain(paramValue2b); @@ -100,15 +100,15 @@ describe('RouteService', () => { }); }); - describe('removeQueryParameter', () => { + describe('getWithoutParameter', () => { it('should return a list of values that does not contain any values for the parameter anymore when the parameter exists', () => { - service.removeQueryParameter(paramName2).subscribe((params) => { + service.resolveRouteWithoutParameter(paramName2).subscribe((params) => { const values = params[paramName2]; expect(values).toEqual({}); }); }); it('should return a list of values that does not contain any values for the parameter when the parameter does not exist', () => { - service.removeQueryParameter(nonExistingParamName).subscribe((params) => { + service.resolveRouteWithoutParameter(nonExistingParamName).subscribe((params) => { const values = params[nonExistingParamName]; expect(values).toEqual({}); }); diff --git a/src/app/shared/route.service.ts b/src/app/shared/route.service.ts index f24fa0d00d..10a7eaecb9 100644 --- a/src/app/shared/route.service.ts +++ b/src/app/shared/route.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { ActivatedRoute, convertToParamMap, Params, } from '@angular/router'; +import { + ActivatedRoute, convertToParamMap, NavigationExtras, Params, + Router, +} from '@angular/router'; import { isNotEmpty } from './empty.util'; @Injectable() @@ -10,7 +13,7 @@ export class RouteService { } getQueryParameterValues(paramName: string): Observable { - return this.route.queryParamMap.map((map) => map.getAll(paramName)); + return this.route.queryParamMap.map((map) => [...map.getAll(paramName)]); } getQueryParameterValue(paramName: string): Observable { @@ -25,31 +28,16 @@ export class RouteService { return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1); } - addQueryParameterValue(paramName: string, paramValue: string): Observable { - return this.route.queryParams.map((currentParams) => { - const newParam = {}; - newParam[paramName] = [...convertToParamMap(currentParams).getAll(paramName), paramValue]; - return Object.assign({}, currentParams, newParam); - }); - } - - removeQueryParameterValue(paramName: string, paramValue: string): Observable { - return this.route.queryParams.map((currentParams) => { - const newParam = {}; - const currentFilterParams = convertToParamMap(currentParams).getAll(paramName); - if (isNotEmpty(currentFilterParams)) { - newParam[paramName] = currentFilterParams.filter((param) => (param !== paramValue)); - } - return Object.assign({}, currentParams, newParam); - }); - } - - removeQueryParameter(paramName: string): Observable { - return this.route.queryParams.map((currentParams) => { - const newParam = {}; - newParam[paramName] = {}; - return Object.assign({}, currentParams, newParam); - }); - + getQueryParamsWithPrefix(prefix: string): Observable { + return this.route.queryParamMap + .map((map) => { + const params = {}; + map.keys + .filter((key) => key.startsWith(prefix)) + .forEach((key) => { + params[key] = [...map.getAll(key)]; + }); + return params; + }); } } diff --git a/src/tsconfig.test.json b/src/tsconfig.test.json index 712ad1ab1c..079eabc561 100644 --- a/src/tsconfig.test.json +++ b/src/tsconfig.test.json @@ -1,4 +1,4 @@ -{ +yarn add{ "extends": "../tsconfig.json", "compilerOptions": { "sourceMap": true diff --git a/yarn.lock b/yarn.lock index b6e461e9b6..e6e0aedf55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,6 +122,12 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz#aa67788e64bfa8652691a77b022b3b4031209113" +"@types/acorn@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.3.tgz#d1f3e738dde52536f9aad3d3380d14e448820afd" + dependencies: + "@types/estree" "*" + "@types/body-parser@*": version "1.16.8" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" @@ -139,6 +145,10 @@ version "0.1.1" resolved "https://registry.yarnpkg.com/@types/deep-freeze/-/deep-freeze-0.1.1.tgz#0e1ee6ceee06f51baeb663deec0bb7780bd72827" +"@types/estree@*": + version "0.0.38" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" + "@types/events@*": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" From a2321274220ac5553d7e5d80d86bbc70261a0e96 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Wed, 4 Apr 2018 14:25:57 +0200 Subject: [PATCH 12/34] fixed some tests --- .../search-facet-filter.component.spec.ts | 99 +++++++------------ .../search-facet-filter.component.ts | 2 +- .../search-filter.service.spec.ts | 2 +- .../search-filters.component.html | 2 +- .../search-filters.component.ts | 9 +- .../search-page.component.spec.ts | 5 + .../search-service/search.service.spec.ts | 17 ++-- .../search-service/search.service.ts | 17 ---- src/app/core/browse/browse.service.spec.ts | 12 +-- src/app/core/config/config.service.spec.ts | 13 ++- src/app/core/data/comcol-data.service.spec.ts | 30 ++---- src/app/core/data/item-data.service.spec.ts | 25 +---- .../core/metadata/metadata.service.spec.ts | 3 +- .../core/shared/hal-endpoint.service.spec.ts | 23 ++--- src/app/core/shared/hal-endpoint.service.ts | 7 +- src/app/shared/route.service.spec.ts | 50 ++++------ .../testing/hal-endpoint-service-stub.ts | 11 +++ src/app/shared/testing/search-service-stub.ts | 10 +- src/tsconfig.test.json | 2 +- 19 files changed, 128 insertions(+), 211 deletions(-) create mode 100644 src/app/shared/testing/hal-endpoint-service-stub.ts diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index bf069eee60..e629925e28 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -10,6 +10,10 @@ import { FilterType } from '../../../search-service/filter-type.model'; import { FacetValue } from '../../../search-service/facet-value.model'; import { FormsModule } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; +import { SearchService } from '../../../search-service/search.service'; +import { SearchServiceStub } from '../../../../shared/testing/search-service-stub'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../core/data/paginated-list'; describe('SearchFacetFilterComponent', () => { let comp: SearchFacetFilterComponent; @@ -40,29 +44,31 @@ describe('SearchFacetFilterComponent', () => { search: '' } ]; + + const searchLink = '/search'; + const selectedValues = [value1, value2]; let filterService; - const page = Observable.of(0) + const page = Observable.of(0); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], declarations: [SearchFacetFilterComponent], providers: [ + { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { - provide: SearchFilterService, - useValue: { - isFilterActiveWithValue: (paramName: string, filterValue: string) => true, - getQueryParamsWith: (paramName: string, filterValue: string) => '', - getQueryParamsWithout: (paramName: string, filterValue: string) => '', - getPage: (paramName: string) => page, - /* tslint:disable:no-empty */ - incrementPage: (filterName: string) => { - }, - resetPage: (filterName: string) => { - }, - /* tslint:enable:no-empty */ - searchLink: '/search', - } - }, + provide: SearchFilterService, useValue: { + isFilterActiveWithValue: (paramName: string, filterValue: string) => true, + getPage: (paramName: string) => page, + /* tslint:disable:no-empty */ + incrementPage: (filterName: string) => { + }, + resetPage: (filterName: string) => { + }, + getSearchOptions: () => Observable.of({}), + + /* tslint:enable:no-empty */ + } + } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(SearchFacetFilterComponent, { @@ -74,7 +80,8 @@ describe('SearchFacetFilterComponent', () => { fixture = TestBed.createComponent(SearchFacetFilterComponent); comp = fixture.componentInstance; // SearchPageComponent test instance comp.filterConfig = mockFilterConfig; - comp.filterValues = values; + comp.filterValues = [Observable.of(new RemoteData(false, false, true, null, new PaginatedList(null, values)))]; + comp.selectedValues = selectedValues; filterService = (comp as any).filterService; fixture.detectChanges(); }); @@ -97,64 +104,24 @@ describe('SearchFacetFilterComponent', () => { }); it('should return the value of the searchLink variable in the filter service', () => { - expect(link).toEqual(filterService.searchLink); + expect(link).toEqual(searchLink); }); }); - describe('when the getQueryParamsWith method is called wih a value', () => { - beforeEach(() => { - spyOn(filterService, 'getQueryParamsWith'); - comp.getQueryParamsWith(values[1].value); - }); - - it('should call getQueryParamsWith on the filterService with the correct filter parameter name and the passed value', () => { - expect(filterService.getQueryParamsWith).toHaveBeenCalledWith(mockFilterConfig, values[1].value) + describe('when the getAddParams method is called wih a value', () => { + it('should return the selectedValueq list with the new parameter value', () => { + const result = comp.getAddParams(value3); + expect(result).toEqual({[mockFilterConfig.paramName]: [value1, value2, value3]}); }); }); - describe('when the getQueryParamsWithout method is called wih a value', () => { - beforeEach(() => { - spyOn(filterService, 'getQueryParamsWithout'); - comp.getQueryParamsWithout(values[1].value); - }); - - it('should call getQueryParamsWithout on the filterService with the correct filter parameter name and the passed value', () => { - expect(filterService.getQueryParamsWithout).toHaveBeenCalledWith(mockFilterConfig, values[1].value) + describe('when the getRemoveParams method is called wih a value', () => { + it('should return the selectedValueq list with the parameter value left out', () => { + const result = comp.getRemoveParams(value1); + expect(result).toEqual({[mockFilterConfig.paramName]: [value2]}); }); }); - describe('when the facetCount method is triggered when there are less items than the amount of pages should display', () => { - let count: Observable; - beforeEach(() => { - comp.currentPage = Observable.of(3); - // 2 x 3 = 6, there are only 3 values - count = comp.facetCount; - }); - - it('should return the correct number of items shown (this equals the total amount of values for this filter)', () => { - const sub = count.subscribe((c) => expect(c).toBe(values.length)); - sub.unsubscribe(); - }); - }); - - describe('when the facetCount method is triggered when there are more items than the amount of pages should display', () => { - let count: Observable; - beforeEach(() => { - comp.currentPage = Observable.of(1); - // 2 x 1 = 2, there are more than 2 (3) items - count = comp.facetCount; - }); - - it('should return the correct number of items shown (this equals the page count x page size)', () => { - const sub = count.subscribe((c) => { - const subsub = comp.currentPage.subscribe((currentPage) => { - expect(c).toBe(currentPage * mockFilterConfig.pageSize); - }); - subsub.unsubscribe() - }); - sub.unsubscribe(); - }); - }); describe('when the showMore method is called', () => { beforeEach(() => { diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index c7941ec5dc..60b7b13b0d 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -102,7 +102,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { isLastPage(): Observable { return Observable.of(false); - // return this.filterValues.flatMap((map) => map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages)); + // return this.filterValues$.flatMap((map) => map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages)); } getRemoveParams(value: string) { diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 853f8b0f68..0ab1e4319d 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -50,7 +50,7 @@ describe('SearchFilterService', () => { }; beforeEach(() => { - service = new SearchFilterService(store, routeServiceStub, searchServiceStub); + service = new SearchFilterService(store, routeServiceStub); }); describe('when the initialCollapse method is triggered', () => { diff --git a/src/app/+search-page/search-filters/search-filters.component.html b/src/app/+search-page/search-filters/search-filters.component.html index 09782b68d4..566450b7f5 100644 --- a/src/app/+search-page/search-filters/search-filters.component.html +++ b/src/app/+search-page/search-filters/search-filters.component.html @@ -4,4 +4,4 @@
-{{"search.filters.reset" | translate}} \ No newline at end of file +{{"search.filters.reset" | translate}} \ No newline at end of file diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index 808ce3be67..517b2e1e59 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -3,6 +3,7 @@ import { SearchService } from '../search-service/search.service'; import { RemoteData } from '../../core/data/remote-data'; import { SearchFilterConfig } from '../search-service/search-filter-config.model'; import { Observable } from 'rxjs/Observable'; +import { SearchFilterService } from './search-filter/search-filter.service'; /** * This component renders a simple item page. @@ -18,12 +19,10 @@ import { Observable } from 'rxjs/Observable'; export class SearchFiltersComponent { filters: Observable>; - constructor(private searchService: SearchService) { + clearParams; + constructor(private searchService: SearchService, private filterService: SearchFilterService) { this.filters = searchService.getConfig(); - } - - getClearFiltersQueryParams(): any { - return this.searchService.getClearFiltersQueryParams(); + this.clearParams = filterService.getCurrentFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;}); } getSearchLink() { diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index d72610695d..8cd041eabb 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -17,6 +17,7 @@ import { ActivatedRoute } from '@angular/router'; import { By } from '@angular/platform-browser'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; +import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; describe('SearchPageComponent', () => { let comp: SearchPageComponent; @@ -89,6 +90,10 @@ describe('SearchPageComponent', () => { provide: SearchSidebarService, useValue: sidebarService }, + { + provide: SearchFilterService, + useValue: {} + }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(SearchPageComponent, { diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index f8a1d73ae1..5f9f67390b 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -15,6 +15,7 @@ import { RequestService } from '../../core/data/request.service'; import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouterStub } from '../../shared/testing/router-stub'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; @Component({ template: '' }) class DummyComponent { @@ -37,14 +38,12 @@ describe('SearchService', () => { DummyComponent ], providers: [ - { provide: ItemDataService, useValue: {} }, - { provide: RouteService, useValue: {} }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: route }, { provide: ResponseCacheService, useValue: {} }, { provide: RequestService, useValue: {} }, - { provide: ActivatedRoute, useValue: route }, { provide: RemoteDataBuildService, useValue: {} }, - { provide: GLOBAL_CONFIG, useValue: {} }, - { provide: Router, useValue: router }, + { provide: HALEndpointService, useValue: {} }, SearchService ], }); @@ -73,14 +72,12 @@ describe('SearchService', () => { DummyComponent ], providers: [ - { provide: ItemDataService, useValue: {} }, - { provide: RouteService, useValue: {} }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: route }, { provide: ResponseCacheService, useValue: {} }, { provide: RequestService, useValue: {} }, - { provide: ActivatedRoute, useValue: route }, { provide: RemoteDataBuildService, useValue: {} }, - { provide: GLOBAL_CONFIG, useValue: {} }, - { provide: Router, useValue: router }, + { provide: HALEndpointService, useValue: {} }, SearchService ], }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 2ef69e8578..134ae657a1 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -23,7 +23,6 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { RouteService } from '../../shared/route.service'; import { NormalizedSearchResult } from '../normalized-search-result.model'; import { SearchOptions } from '../search-options.model'; import { SearchResult } from '../search-result.model'; @@ -37,7 +36,6 @@ import { getSearchResultFor } from './search-result-element-decorator'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service'; import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service'; -import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; @Injectable() @@ -206,7 +204,6 @@ export class SearchService implements OnDestroy { } getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable>> { - console.log('facetvalues'); const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe( map((url: string) => { const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`]; @@ -268,20 +265,6 @@ export class SearchService implements OnDestroy { this.router.navigate([this.uiSearchRoute], navigationExtras); } - getClearFiltersQueryParams(): any { - const params = {}; - this.sub = this.route.queryParamMap - .subscribe((pmap) => { - pmap.keys - .filter((key) => this.config - .findIndex((conf: SearchFilterConfig) => conf.paramName === key) < 0) - .forEach((key) => { - params[key] = pmap.get(key); - }) - }); - return params; - } - getSearchLink() { return this.uiSearchRoute; } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 2385948b2f..2e9163fbac 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -3,11 +3,11 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca import { BrowseService } from './browse.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; -import { GlobalConfig } from '../../../config'; import { hot, cold, getTestScheduler } from 'jasmine-marbles'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEndpointRequest } from '../data/request.models'; import { TestScheduler } from 'rxjs/Rx'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; describe('BrowseService', () => { let scheduler: TestScheduler; @@ -15,8 +15,8 @@ describe('BrowseService', () => { let responseCache: ResponseCacheService; let requestService: RequestService; - const envConfig = {} as GlobalConfig; const browsesEndpointURL = 'https://rest.api/browses'; + const halService: any = new HALEndpointServiceStub(browsesEndpointURL); const browseDefinitions = [ Object.assign(new BrowseDefinition(), { metadataBrowse: false, @@ -91,7 +91,7 @@ describe('BrowseService', () => { return new BrowseService( responseCache, requestService, - envConfig + halService ); } @@ -106,7 +106,7 @@ describe('BrowseService', () => { responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); service = initTestService(); - spyOn(service, 'getEndpoint').and + spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); }); @@ -171,7 +171,7 @@ describe('BrowseService', () => { responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); service = initTestService(); - spyOn(service, 'getEndpoint').and + spyOn(halService, 'getEndpoint').and .returnValue(hot('----')); const metadatumKey = 'dc.date.issued'; @@ -188,7 +188,7 @@ describe('BrowseService', () => { responseCache = initMockResponseCacheService(false); requestService = getMockRequestService(); service = initTestService(); - spyOn(service, 'getEndpoint').and + spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); const metadatumKey = 'dc.date.issued'; diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 3cdb22948f..4b05d5c929 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,11 +1,12 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/Rx'; -import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; import { ConfigRequest, FindAllOptions } from '../data/request.models'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; @@ -17,8 +18,7 @@ class TestService extends ConfigService { constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, - protected EnvConfig: GlobalConfig - ) { + protected halService: HALEndpointService) { super(); } } @@ -28,8 +28,8 @@ describe('ConfigService', () => { let service: TestService; let responseCache: ResponseCacheService; let requestService: RequestService; + let halService: any; - const envConfig = {} as GlobalConfig; const findOptions: FindAllOptions = new FindAllOptions(); const scopeName = 'traditional'; @@ -51,7 +51,7 @@ describe('ConfigService', () => { return new TestService( responseCache, requestService, - envConfig + halService ); } @@ -60,8 +60,7 @@ describe('ConfigService', () => { requestService = getMockRequestService(); service = initTestService(); scheduler = getTestScheduler(); - spyOn(service, 'getEndpoint').and - .returnValue(hot('--a-', { a: serviceEndpoint })); + halService = new HALEndpointServiceStub(configEndpoint); }); describe('getConfigByHref', () => { diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index cf43482ba5..b5727fb22f 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -13,6 +13,7 @@ import { CommunityDataService } from './community-data.service'; import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; const LINK_NAME = 'test'; @@ -21,7 +22,6 @@ class NormalizedTestObject extends NormalizedObject { } class TestService extends ComColDataService { - protected linkPath = LINK_NAME; constructor( protected responseCache: ResponseCacheService, @@ -30,7 +30,9 @@ class TestService extends ComColDataService { protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected linkPath: string ) { super(); } @@ -44,6 +46,7 @@ describe('ComColDataService', () => { let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; + const halService: any = {}; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; @@ -90,7 +93,9 @@ describe('ComColDataService', () => { store, EnvConfig, cds, - objectCache + objectCache, + halService, + LINK_NAME ); } @@ -154,24 +159,5 @@ describe('ComColDataService', () => { }); }); - describe('if the scope is not specified', () => { - beforeEach(() => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(); - objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); - service = initTestService(); - }); - - it('should return this.getEndpoint()', () => { - spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint })); - - const result = service.getScopedEndpoint(undefined); - const expected = cold('--f-', { f: serviceEndpoint }); - - expect(result).toBeObservable(expected); - }); - }); - }); }); diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 7d610bfaae..4d0dc8aec3 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,24 +1,23 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/Rx'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; let service: ItemDataService; let bs: BrowseService; - const requestService = {} as RequestService; const responseCache = {} as ResponseCacheService; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; - const EnvConfig = {} as GlobalConfig; + const halEndpointService = {} as HALEndpointService; const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; const browsesEndpoint = 'https://rest.api/discover/browses'; @@ -42,8 +41,8 @@ describe('ItemDataService', () => { requestService, rdbService, store, - EnvConfig, - bs + bs, + halEndpointService ); } @@ -74,21 +73,5 @@ describe('ItemDataService', () => { expect(result).toBeObservable(expected); }); }); - - describe('if the scope is not specified', () => { - beforeEach(() => { - bs = initMockBrowseService(true); - service = initTestService(); - spyOn(service, 'getEndpoint').and.returnValue(cold('--b-', { b: serviceEndpoint })) - }); - - it('should return this.getEndpoint()', () => { - const result = service.getScopedEndpoint(undefined); - const expected = cold('--c-', { c: serviceEndpoint }); - - expect(result).toBeObservable(expected); - }); - }); - }); }); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 4182587cc7..f8f36a358e 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -33,7 +33,7 @@ import { Item } from '../../core/shared/item.model'; import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; -import { PageInfo } from '../shared/page-info.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -114,6 +114,7 @@ describe('MetadataService', () => { { provide: RequestService, useValue: requestService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, + { provide: HALEndpointService, useValue: {}}, Meta, Title, ItemDataService, diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 7c73e15fab..479e15b52a 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -15,15 +15,15 @@ describe('HALEndpointService', () => { const endpointMap = { test: 'https://rest.api/test', }; + const linkPath = 'test'; /* tslint:disable:no-shadowed-variable */ class TestService extends HALEndpointService { - protected linkPath = 'test'; - constructor(protected responseCache: ResponseCacheService, - protected requestService: RequestService, - protected EnvConfig: GlobalConfig) { - super(); + constructor(private responseCache: ResponseCacheService, + private requestService: RequestService, + private EnvConfig: GlobalConfig) { + super(responseCache, requestService, EnvConfig); } } @@ -83,17 +83,16 @@ describe('HALEndpointService', () => { it('should return the endpoint URL for the service\'s linkPath', () => { spyOn(service as any, 'getEndpointAt').and .returnValue(hot('a-', { a: 'https://rest.api/test' })); - const result = service.getEndpoint(); + const result = service.getEndpoint(linkPath); const expected = cold('b-', { b: endpointMap.test }); expect(result).toBeObservable(expected); }); it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { - (service as any).linkPath = 'unknown'; spyOn(service as any, 'getEndpointAt').and .returnValue(hot('a-', { a: undefined })); - const result = service.getEndpoint(); + const result = service.getEndpoint('unknown'); const expected = cold('b-', { b: undefined }); expect(result).toBeObservable(expected); }); @@ -113,7 +112,7 @@ describe('HALEndpointService', () => { spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('----')); - const result = service.isEnabledOnRestApi(); + const result = service.isEnabledOnRestApi(linkPath); const expected = cold('b---', { b: undefined }); expect(result).toBeObservable(expected); }); @@ -121,8 +120,7 @@ describe('HALEndpointService', () => { it('should return true if the service\'s linkPath is in the endpoint map', () => { spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); - - const result = service.isEnabledOnRestApi(); + const result = service.isEnabledOnRestApi(linkPath); const expected = cold('b-c-', { b: undefined, c: true }); expect(result).toBeObservable(expected); }); @@ -131,8 +129,7 @@ describe('HALEndpointService', () => { spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); - (service as any).linkPath = 'unknown'; - const result = service.isEnabledOnRestApi(); + const result = service.isEnabledOnRestApi('unknown'); const expected = cold('b-c-', { b: undefined, c: false }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index c81c3c792d..3bedeb9915 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -14,12 +14,9 @@ import { GLOBAL_CONFIG } from '../../../config'; @Injectable() export class HALEndpointService { - protected linkPath: string; - constructor(private responseCache: ResponseCacheService, private requestService: RequestService, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { - } protected getRootHref(): string { @@ -41,9 +38,7 @@ export class HALEndpointService { } public getEndpoint(linkPath: string): Observable { - const test = this.getEndpointAt(...linkPath.split('/')); - // test.subscribe((test) => console.log(linkPath, test)); - return test; + return this.getEndpointAt(...linkPath.split('/')); } private getEndpointAt(...path: string[]): Observable { diff --git a/src/app/shared/route.service.spec.ts b/src/app/shared/route.service.spec.ts index 00b7d78be6..b134771b3e 100644 --- a/src/app/shared/route.service.spec.ts +++ b/src/app/shared/route.service.spec.ts @@ -67,50 +67,36 @@ describe('RouteService', () => { }); }); - describe('addQueryParameterValue', () => { - it('should return a list of values that contains the added value when a new value is added and the parameter did not exist yet', () => { - service.resolveRouteWithParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => { - expect(params[nonExistingParamName]).toContain(nonExistingParamValue); + describe('getQueryParameterValues', () => { + it('should return a list of values when the parameter exists', () => { + service.getQueryParameterValues(paramName2).subscribe((params) => { + expect(params).toEqual([paramValue2a, paramValue2b]); }); }); - it('should return a list of values that contains the existing values and the added value when a new value is added and the parameter already has values', () => { - service.resolveRouteWithParameterValue(paramName1, nonExistingParamValue).subscribe((params) => { - const values = params[paramName1]; - expect(values).toContain(paramValue1); - expect(values).toContain(nonExistingParamValue); + + it('should return an empty array when the parameter does not exists', () => { + service.getQueryParameterValues(nonExistingParamName).subscribe((params) => { + expect(params).toEqual([]); }); }); }); - describe('removeQueryParameterValue', () => { - it('should return a list of values that does not contain the removed value when the parameter value exists', () => { - service.resolveRouteWithoutParameterValue(paramName2, paramValue2a).subscribe((params) => { - const values = params[paramName2]; - expect(values).toContain(paramValue2b); - expect(values).not.toContain(paramValue2a); + describe('getQueryParameterValue', () => { + it('should return a single value when the parameter exists', () => { + service.getQueryParameterValue(paramName1).subscribe((params) => { + expect(params).toEqual(paramValue1); }); }); - it('should return a list of values that does contain all existing values when the removed parameter does not exist', () => { - service.resolveRouteWithoutParameterValue(paramName2, nonExistingParamValue).subscribe((params) => { - const values = params[paramName2]; - expect(values).toContain(paramValue2a); - expect(values).toContain(paramValue2b); + it('should return only the first value when the parameter exists', () => { + service.getQueryParameterValue(paramName2).subscribe((params) => { + expect(params).toEqual(paramValue2a); }); }); - }); - describe('getWithoutParameter', () => { - it('should return a list of values that does not contain any values for the parameter anymore when the parameter exists', () => { - service.resolveRouteWithoutParameter(paramName2).subscribe((params) => { - const values = params[paramName2]; - expect(values).toEqual({}); - }); - }); - it('should return a list of values that does not contain any values for the parameter when the parameter does not exist', () => { - service.resolveRouteWithoutParameter(nonExistingParamName).subscribe((params) => { - const values = params[nonExistingParamName]; - expect(values).toEqual({}); + it('should return undefined when the parameter exists', () => { + service.getQueryParameterValue(nonExistingParamName).subscribe((params) => { + expect(params).toBeNull(); }); }); }); diff --git a/src/app/shared/testing/hal-endpoint-service-stub.ts b/src/app/shared/testing/hal-endpoint-service-stub.ts new file mode 100644 index 0000000000..e7dbe8bea1 --- /dev/null +++ b/src/app/shared/testing/hal-endpoint-service-stub.ts @@ -0,0 +1,11 @@ +import { Observable } from 'rxjs/Observable'; +import { ViewMode } from '../../+search-page/search-options.model'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +export class HALEndpointServiceStub { + + constructor(private url: string) {}; + getEndpoint(path: string) { + return Observable.of(this.url + '/' + path); + } +} diff --git a/src/app/shared/testing/search-service-stub.ts b/src/app/shared/testing/search-service-stub.ts index 23b2004827..7ad0d871ce 100644 --- a/src/app/shared/testing/search-service-stub.ts +++ b/src/app/shared/testing/search-service-stub.ts @@ -9,7 +9,7 @@ export class SearchServiceStub { viewMode = this.subject.asObservable(); - constructor() { + constructor(private searchLink: string = '/search') { this.setViewMode(ViewMode.List); } @@ -21,6 +21,10 @@ export class SearchServiceStub { this.testViewMode = viewMode; } + getFacetValuesFor() { + return null; + } + get testViewMode(): ViewMode { return this._viewMode; } @@ -29,4 +33,8 @@ export class SearchServiceStub { this._viewMode = viewMode; this.subject.next(viewMode); } + + getSearchLink() { + return this.searchLink; + } } diff --git a/src/tsconfig.test.json b/src/tsconfig.test.json index 079eabc561..712ad1ab1c 100644 --- a/src/tsconfig.test.json +++ b/src/tsconfig.test.json @@ -1,4 +1,4 @@ -yarn add{ +{ "extends": "../tsconfig.json", "compilerOptions": { "sourceMap": true From 79caf1533c3e90a31ab8b15b360187f4df359dc9 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 5 Apr 2018 08:49:36 +0200 Subject: [PATCH 13/34] fixes for existing tested --- .../search-facet-filter.component.html | 4 +- .../search-filter.service.spec.ts | 32 ------- .../search-filters.component.spec.ts | 16 ++-- .../+search-page/search-page.component.html | 18 ++-- .../search-page.component.spec.ts | 85 +++++-------------- src/app/+search-page/search-page.component.ts | 43 +++------- .../core/shared/hal-endpoint.service.spec.ts | 18 +--- .../search-form/search-form.component.spec.ts | 2 +- .../search-form/search-form.component.ts | 6 +- 9 files changed, 56 insertions(+), 168 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html index aa71c5c24c..8687aeb25b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html @@ -32,7 +32,7 @@ [action]="getCurrentUrl()"> + [placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate" [ngModelOptions]="{standalone: true}"/> - \ No newline at end of file + diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 0ab1e4319d..26eb961c53 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -159,28 +159,6 @@ describe('SearchFilterService', () => { }); }); - describe('when the getQueryParamsWithout method is called', () => { - beforeEach(() => { - spyOn(routeServiceStub, 'removeQueryParameterValue'); - service.getQueryParamsWithout(mockFilterConfig, value1); - }); - - it('should call removeQueryParameterValue on the route service with the same parameters', () => { - expect(routeServiceStub.removeQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1); - }); - }); - - describe('when the getQueryParamsWith method is called', () => { - beforeEach(() => { - spyOn(routeServiceStub, 'addQueryParameterValue'); - service.getQueryParamsWith(mockFilterConfig, value1); - }); - - it('should call addQueryParameterValue on the route service with the same parameters', () => { - expect(routeServiceStub.addQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1); - }); - }); - describe('when the getSelectedValuesForFilter method is called', () => { beforeEach(() => { spyOn(routeServiceStub, 'getQueryParameterValues'); @@ -192,14 +170,4 @@ describe('SearchFilterService', () => { }); }); - describe('when the uiSearchRoute method is called', () => { - let link: string; - beforeEach(() => { - link = service.searchLink; - }); - - it('should return the value of uiSearchRoute in the search service', () => { - expect(link).toEqual(searchServiceStub.uiSearchRoute); - }); - }); }); diff --git a/src/app/+search-page/search-filters/search-filters.component.spec.ts b/src/app/+search-page/search-filters/search-filters.component.spec.ts index 0bdee94634..64c2ea5332 100644 --- a/src/app/+search-page/search-filters/search-filters.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filters.component.spec.ts @@ -4,6 +4,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SearchFilterService } from './search-filter/search-filter.service'; import { SearchFiltersComponent } from './search-filters.component'; import { SearchService } from '../search-service/search.service'; import { Observable } from 'rxjs/Observable'; @@ -22,6 +23,9 @@ describe('SearchFiltersComponent', () => { } /* tslint:enable:no-empty */ }; + const searchFilterServiceStub = jasmine.createSpyObj('SearchFilterService', { + getCurrentFilters: Observable.of({}) + }); beforeEach(async(() => { TestBed.configureTestingModule({ @@ -29,6 +33,7 @@ describe('SearchFiltersComponent', () => { declarations: [SearchFiltersComponent], providers: [ { provide: SearchService, useValue: searchServiceStub }, + { provide: SearchFilterService, useValue: searchFilterServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] @@ -44,17 +49,6 @@ describe('SearchFiltersComponent', () => { searchService = (comp as any).searchService; }); - describe('when the getClearFiltersQueryParams method is called', () => { - beforeEach(() => { - spyOn(searchService, 'getClearFiltersQueryParams'); - comp.getClearFiltersQueryParams(); - }); - - it('should call getClearFiltersQueryParams on the searchService', () => { - expect(searchService.getClearFiltersQueryParams).toHaveBeenCalled() - }); - }); - describe('when the getSearchLink method is called', () => { beforeEach(() => { spyOn(searchService, 'getSearchLink'); diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 81f0c78527..c50bb30696 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -1,22 +1,22 @@
- + [resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements">
+ [scopes]="(scopeListRD$ | async)?.payload?.page">
- @@ -29,8 +29,8 @@ | translate}}
- +
diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index 8cd041eabb..cc53e18871 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -4,12 +4,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; +import { cold, hot } from 'jasmine-marbles'; import { Observable } from 'rxjs/Observable'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CommunityDataService } from '../core/data/community-data.service'; import { Community } from '../core/shared/community.model'; import { HostWindowService } from '../shared/host-window.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { PaginatedSearchOptions } from './paginated-search-options.model'; import { SearchPageComponent } from './search-page.component'; import { SearchService } from './search-service/search.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -35,12 +37,17 @@ describe('SearchPageComponent', () => { pagination.pageSize = 10; const sort: SortOptions = new SortOptions(); const mockResults = Observable.of(['test', 'data']); - const searchServiceStub = { - searchOptions:{ pagination: pagination, sort: sort }, - search: () => mockResults - }; + const searchServiceStub = jasmine.createSpyObj('SearchService', { + search: mockResults + }); const queryParam = 'test query'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; + const paginatedSearchOptions = { + query: queryParam, + scope: scopeParam, + pagination, + sort + }; const activatedRouteStub = { queryParams: Observable.of({ query: queryParam, @@ -51,20 +58,8 @@ describe('SearchPageComponent', () => { isCollapsed: Observable.of(true), collapse: () => this.isCollapsed = Observable.of(true), expand: () => this.isCollapsed = Observable.of(false) - } - - const mockCommunityList = []; - const communityDataServiceStub = { - findAll: () => Observable.of(mockCommunityList), - findById: () => Observable.of(new Community()) }; - class RouterStub { - navigateByUrl(url: string) { - return url; - } - } - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()], @@ -92,7 +87,11 @@ describe('SearchPageComponent', () => { }, { provide: SearchFilterService, - useValue: {} + useValue: jasmine.createSpyObj('SearchFilterService', { + getPaginatedSearchOptions: hot('a', { + a: paginatedSearchOptions + }) + }) }, ], schemas: [NO_ERRORS_SCHEMA] @@ -108,54 +107,10 @@ describe('SearchPageComponent', () => { searchServiceObject = (comp as any).service; }); - it('should set the scope and query based on the route parameters', () => { - expect(comp.query).toBe(queryParam); - expect((comp as any).scope).toBe(scopeParam); - }); - - describe('when update search results is called', () => { - let paginationUpdate; - let sortUpdate; - beforeEach(() => { - paginationUpdate = Object.assign( - {}, - new PaginationComponentOptions(), - { - currentPage: 5, - pageSize: 15 - } - ); - sortUpdate = Object.assign({}, - new SortOptions(), - { - direction: SortDirection.Ascending, - field: 'test-field' - } - ); - }); - - it('should call the search function of the search service with the right parameters', () => { - spyOn(searchServiceObject, 'search').and.callThrough(); - - (comp as any).updateSearchResults({ - pagination: pagination, - sort: sort - }); - - expect(searchServiceObject.search).toHaveBeenCalledWith(queryParam, scopeParam, { - pagination: pagination, - sort: sort - }); - }); - - it('should update the results', () => { - spyOn(searchServiceObject, 'search').and.callThrough(); - - (comp as any).updateSearchResults({}); - - expect(comp.resultsRDObs as any).toBe(mockResults); - }); - + it('should get the scope and query from the route parameters', () => { + expect(comp.searchOptions$).toBeObservable(cold('b', { + b: paginatedSearchOptions + })); }); describe('when the closeSidebar event is emitted clicked in mobile view', () => { diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 57fb02b730..2a903f884a 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; +import { flatMap, } from 'rxjs/operators'; import { SortOptions } from '../core/cache/models/sort-options.model'; import { CommunityDataService } from '../core/data/community-data.service'; import { PaginatedList } from '../core/data/paginated-list'; @@ -8,14 +8,12 @@ import { RemoteData } from '../core/data/remote-data'; import { Community } from '../core/shared/community.model'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { pushInOut } from '../shared/animations/push'; -import { isNotEmpty } from '../shared/empty.util'; import { HostWindowService } from '../shared/host-window.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SearchOptions, ViewMode } from './search-options.model'; +import { PaginatedSearchOptions } from './paginated-search-options.model'; +import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; import { SearchResult } from './search-result.model'; import { SearchService } from './search-service/search.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; -import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; /** * This component renders a simple item page. @@ -30,19 +28,14 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte changeDetection: ChangeDetectionStrategy.OnPush, animations: [pushInOut] }) -export class SearchPageComponent implements OnInit, OnDestroy { +export class SearchPageComponent implements OnInit { - private sub; - private scope: string; - - query: string; - scopeObjectRDObs: Observable>; - resultsRDObs: Observable>>>; + resultsRD$: Observable>>>; currentParams = {}; - searchOptions: SearchOptions; + searchOptions$: Observable; sortConfig: SortOptions; - scopeListRDObs: Observable>>; - isMobileView: Observable; + scopeListRD$: Observable>>; + isMobileView$: Observable; pageSize; pageSizeOptions; defaults = { @@ -58,27 +51,19 @@ export class SearchPageComponent implements OnInit, OnDestroy { private sidebarService: SearchSidebarService, private windowService: HostWindowService, private filterService: SearchFilterService) { - this.isMobileView = Observable.combineLatest( + this.isMobileView$ = Observable.combineLatest( this.windowService.isXs(), this.windowService.isSm(), ((isXs, isSm) => isXs || isSm) ); - this.scopeListRDObs = communityService.findAll(); + this.scopeListRD$ = communityService.findAll(); } ngOnInit(): void { - this.sub = this.filterService.getPaginatedSearchOptions(this.defaults).subscribe((options) => { - this.updateSearchResults(options); - }); - } - - private updateSearchResults(searchOptions) { - this.resultsRDObs = this.service.search(searchOptions); - this.searchOptions = searchOptions; - } - - ngOnDestroy() { - this.sub.unsubscribe(); + this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults); + this.resultsRD$ = this.searchOptions$.pipe( + flatMap((searchOptions) => this.service.search(searchOptions)) + ); } public closeSidebar(): void { diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 479e15b52a..0c2afe938b 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -17,18 +17,6 @@ describe('HALEndpointService', () => { }; const linkPath = 'test'; - /* tslint:disable:no-shadowed-variable */ - class TestService extends HALEndpointService { - - constructor(private responseCache: ResponseCacheService, - private requestService: RequestService, - private EnvConfig: GlobalConfig) { - super(responseCache, requestService, EnvConfig); - } - } - - /* tslint:enable:no-shadowed-variable */ - describe('getRootEndpointMap', () => { beforeEach(() => { responseCache = jasmine.createSpyObj('responseCache', { @@ -45,7 +33,7 @@ describe('HALEndpointService', () => { rest: { baseUrl: 'https://rest.api/' } } as any; - service = new TestService( + service = new HALEndpointService( responseCache, requestService, envConfig @@ -73,7 +61,7 @@ describe('HALEndpointService', () => { rest: { baseUrl: 'https://rest.api/' } } as any; - service = new TestService( + service = new HALEndpointService( responseCache, requestService, envConfig @@ -100,7 +88,7 @@ describe('HALEndpointService', () => { describe('isEnabledOnRestApi', () => { beforeEach(() => { - service = new TestService( + service = new HALEndpointService( responseCache, requestService, envConfig diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index ee1a8cd8f5..d148429b01 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -69,7 +69,7 @@ describe('SearchFormComponent', () => { fixture.detectChanges(); const testCommunity = objects[1]; - comp.scope = testCommunity; + comp.scope = testCommunity.id; fixture.detectChanges(); tick(); diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index fb3c6ba5a2..478df3bb65 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -22,10 +22,8 @@ export class SearchFormComponent { @Input() scopes: DSpaceObject[]; @Input() - set scope(dso: DSpaceObject) { - if (hasValue(dso)) { - this.selectedId = dso.id; - } + set scope(id: string) { + this.selectedId = id; } constructor(private router: Router) { From 6133bc5068a37a8d3f0bd16c22b35ecca5cb63c1 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 5 Apr 2018 09:51:46 +0200 Subject: [PATCH 14/34] fixed tslint error --- .../search-facet-filter/search-facet-filter.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index e629925e28..c3dcff8368 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -122,7 +122,6 @@ describe('SearchFacetFilterComponent', () => { }); }); - describe('when the showMore method is called', () => { beforeEach(() => { spyOn(filterService, 'incrementPage'); From d5114a87dab77fd8aec872ca684ed38b06362439 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 5 Apr 2018 15:54:51 +0200 Subject: [PATCH 15/34] search page fix --- .../search-filters/search-filter/search-filter.service.ts | 4 ++-- src/app/+search-page/search-page.component.ts | 3 ++- src/app/+search-page/search-service/search.service.ts | 2 +- src/app/shared/pagination/pagination.component.ts | 3 ++- src/app/shared/search-form/search-form.component.html | 2 +- src/app/shared/search-form/search-form.component.ts | 7 ------- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 9c5e406a78..1ba8182d33 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -79,13 +79,13 @@ export class SearchFilterService { this.getCurrentQuery(), this.getCurrentFilters(), (pagination, sort, view, scope, query, filters) => { - return Object.assign(new SearchOptions(), + return Object.assign(new PaginatedSearchOptions(), defaults, { pagination: pagination, sort: sort, view: view, - scope: scope, + scope: scope || defaults.scope, query: query, filters: filters }) diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 2a903f884a..c9efc65676 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -43,7 +43,8 @@ export class SearchPageComponent implements OnInit { id: 'search-results-pagination', pageSize: 10 }, - query: '' + query: '', + scope: '' }; constructor(private service: SearchService, diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 134ae657a1..42f527a411 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -99,7 +99,7 @@ export class SearchService implements OnDestroy { const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { if (hasValue(searchOptions)) { - url = searchOptions.toRestUrl(url); + url = (searchOptions as PaginatedSearchOptions).toRestUrl(url); } const request = new GetRequest(this.requestService.generateRequestId(), url); return Object.assign(request, { diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 2fa70ddd18..6ab0e2a567 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -336,7 +336,8 @@ export class PaginationComponent implements OnDestroy, OnInit { */ private updateRoute(params: {}) { this.router.navigate([], { - queryParams: Object.assign({}, this.currentQueryParams, params) + queryParams: Object.assign({}, this.currentQueryParams, params), + queryParamsHandling: 'merge' }); } diff --git a/src/app/shared/search-form/search-form.component.html b/src/app/shared/search-form/search-form.component.html index a4e8cddc1e..8bca5aabeb 100644 --- a/src/app/shared/search-form/search-form.component.html +++ b/src/app/shared/search-form/search-form.component.html @@ -1,6 +1,6 @@
- diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 478df3bb65..49f825e28c 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -50,11 +50,4 @@ export class SearchFormComponent { return isNotEmpty(object); } - byId(id1: string, id2: string) { - if (isEmpty(id1) && isEmpty(id2)) { - return true; - } - return id1 === id2; - } - } From e6b4381f4573c954fbe09c95059203718d46ea9a Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 5 Apr 2018 16:34:49 +0200 Subject: [PATCH 16/34] fixed pagination test --- src/app/shared/pagination/pagination.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index b108074893..ad05f0cdfe 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -271,7 +271,7 @@ describe('Pagination component', () => { changePage(testFixture, 3); tick(); - expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 'ASC', sortField: 'dc.title' } }); + expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 'ASC', sortField: 'dc.title' }, queryParamsHandling: 'merge' }); })); @@ -282,7 +282,7 @@ describe('Pagination component', () => { changePageSize(testFixture, '20'); tick(); - expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 1, pageSize: 20, sortDirection: 'ASC', sortField: 'dc.title' } }); + expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 1, pageSize: 20, sortDirection: 'ASC', sortField: 'dc.title' } , queryParamsHandling: 'merge' }); })); it('should set correct values', fakeAsync(() => { From 7caff8ca8ae9149a65658d512f5f56a24a931f77 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Mon, 9 Apr 2018 11:34:34 +0200 Subject: [PATCH 17/34] added missing tests for the search-facet-filter component --- .../search-facet-filter.component.spec.ts | 96 +++++++++++++++++-- .../search-facet-filter.component.ts | 11 ++- src/app/shared/testing/router-stub.ts | 4 + 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index c3dcff8368..25a6d90c17 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -14,6 +14,10 @@ import { SearchService } from '../../../search-service/search.service'; import { SearchServiceStub } from '../../../../shared/testing/search-service-stub'; import { RemoteData } from '../../../../core/data/remote-data'; import { PaginatedList } from '../../../../core/data/paginated-list'; +import { SearchOptions } from '../../../search-options.model'; +import { RouterStub } from '../../../../shared/testing/router-stub'; +import { Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; describe('SearchFacetFilterComponent', () => { let comp: SearchFacetFilterComponent; @@ -48,13 +52,18 @@ describe('SearchFacetFilterComponent', () => { const searchLink = '/search'; const selectedValues = [value1, value2]; let filterService; + let searchService; + let router; const page = Observable.of(0); + + const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(null, values))); beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], + imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], declarations: [SearchFacetFilterComponent], providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, + { provide: Router, useValue: new RouterStub() }, { provide: SearchFilterService, useValue: { isFilterActiveWithValue: (paramName: string, filterValue: string) => true, @@ -65,7 +74,6 @@ describe('SearchFacetFilterComponent', () => { resetPage: (filterName: string) => { }, getSearchOptions: () => Observable.of({}), - /* tslint:enable:no-empty */ } } @@ -80,9 +88,13 @@ describe('SearchFacetFilterComponent', () => { fixture = TestBed.createComponent(SearchFacetFilterComponent); comp = fixture.componentInstance; // SearchPageComponent test instance comp.filterConfig = mockFilterConfig; - comp.filterValues = [Observable.of(new RemoteData(false, false, true, null, new PaginatedList(null, values)))]; + comp.filterValues = [mockValues]; + comp.filterValues$ = new BehaviorSubject(comp.filterValues); comp.selectedValues = selectedValues; filterService = (comp as any).filterService; + searchService = (comp as any).searchService; + spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues); + router = (comp as any).router; fixture.detectChanges(); }); @@ -111,14 +123,14 @@ describe('SearchFacetFilterComponent', () => { describe('when the getAddParams method is called wih a value', () => { it('should return the selectedValueq list with the new parameter value', () => { const result = comp.getAddParams(value3); - expect(result).toEqual({[mockFilterConfig.paramName]: [value1, value2, 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', () => { const result = comp.getRemoveParams(value1); - expect(result).toEqual({[mockFilterConfig.paramName]: [value2]}); + expect(result).toEqual({ [mockFilterConfig.paramName]: [value2] }); }); }); @@ -140,7 +152,7 @@ describe('SearchFacetFilterComponent', () => { }); it('should call resetPage on the filterService with the correct filter parameter name', () => { - expect(filterService.resetPage).toHaveBeenCalledWith(mockFilterConfig.name) + expect(filterService.resetPage).toHaveBeenCalledWith(mockFilterConfig.name); }); }); @@ -154,4 +166,76 @@ describe('SearchFacetFilterComponent', () => { expect(filterService.getPage).toHaveBeenCalledWith(mockFilterConfig.name) }); }); + + describe('when the getCurrentUrl method is called', () => { + const url = 'test.url/test' + beforeEach(() => { + router.navigateByUrl(url); + }); + + it('should call getPage on the filterService with the correct filter parameter name', () => { + expect(router.url).toEqual(url); + }); + }); + + describe('when the onSubmit method is called with data', () => { + const searchUrl = '/search/path'; + const testValue = 'test'; + const data = { [mockFilterConfig.paramName]: testValue }; + beforeEach(() => { + spyOn(comp, 'getSearchLink').and.returnValue(searchUrl); + comp.onSubmit(data); + }); + + it('should call navigate on the router with the right searchlink and parameters', () => { + expect(router.navigate).toHaveBeenCalledWith([searchUrl], { + queryParams: { [mockFilterConfig.paramName]: [...selectedValues, testValue] }, + queryParamsHandling: 'merge' + }); + }); + }); + + describe('when updateFilterValueList is called', () => { + const cPage = 10; + const searchOptions = new SearchOptions(); + beforeEach(() => { + // spyOn(searchService, 'getFacetValuesFor'); Already spied upon + comp.currentPage = Observable.of(cPage); + comp.updateFilterValueList(searchOptions); + }); + + it('should call getFacetValuesFor on the searchService with the correct parameters', () => { + expect(searchService.getFacetValuesFor).toHaveBeenCalledWith(mockFilterConfig, cPage, searchOptions); + }); + }); + + describe('when updateFilterValueList is called and pageChange is set to true', () => { + const searchOptions = new SearchOptions(); + beforeEach(() => { + comp.pageChange = true; + spyOn(comp, 'showFirstPageOnly'); + comp.updateFilterValueList(searchOptions); + }); + + it('should not call showFirstPageOnly on the component', () => { + expect(comp.showFirstPageOnly).not.toHaveBeenCalled(); + }); + + it('should set pageChange to false', () => { + expect(comp.pageChange).toBeFalsy(); + }); + }); + + describe('when updateFilterValueList is called and pageChange is set to false', () => { + const searchOptions = new SearchOptions(); + beforeEach(() => { + comp.pageChange = false; + spyOn(comp, 'showFirstPageOnly'); + comp.updateFilterValueList(searchOptions); + }); + + it('should call showFirstPageOnly on the component', () => { + expect(comp.showFirstPageOnly).toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 60b7b13b0d..04258fbd35 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -57,7 +57,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.filterValues = [...this.filterValues, newValues$]; this.filterValues$.next(this.filterValues); }); - // this.filterValues.subscribe((c) => c.map((a) => a.subscribe((b) => console.log(b)))); } isChecked(value: FacetValue): Observable { @@ -101,8 +100,14 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { } isLastPage(): Observable { - return Observable.of(false); - // return this.filterValues$.flatMap((map) => map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages)); + return this.filterValues$.flatMap((map) => { + + if (isNotEmpty(map)) { + return map.pop().map((rd: RemoteData>) => rd.payload.currentPage >= rd.payload.totalPages); + } else { + return false; + } + }); } getRemoveParams(value: string) { diff --git a/src/app/shared/testing/router-stub.ts b/src/app/shared/testing/router-stub.ts index 6a560d9574..ca4f7642b8 100644 --- a/src/app/shared/testing/router-stub.ts +++ b/src/app/shared/testing/router-stub.ts @@ -1,4 +1,8 @@ export class RouterStub { + url: string; //noinspection TypeScriptUnresolvedFunction navigate = jasmine.createSpy('navigate'); + navigateByUrl(url): void { + this.url = url; + } } From c9e7cdcf9a20399a549242f18584a6ce0ae2db0e Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Mon, 9 Apr 2018 16:29:12 +0200 Subject: [PATCH 18/34] added search service test --- .../search-service/search.service.spec.ts | 138 +++++++++++++++++- .../search-service/search.service.ts | 2 +- src/app/shared/mocks/mock-request.service.ts | 4 +- .../mocks/mock-response-cache.service.ts | 12 +- 4 files changed, 143 insertions(+), 13 deletions(-) diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 5f9f67390b..ef6efe3deb 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -16,6 +16,22 @@ import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouterStub } from '../../shared/testing/router-stub'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { Observable } from 'rxjs/Observable'; +import { PaginatedSearchOptions } from '../paginated-search-options.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { SearchResult } from '../search-result.model'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { RequestEntry } from '../../core/data/request.reducer'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; +import { + FacetConfigSuccessResponse, RestResponse, + SearchSuccessResponse +} from '../../core/cache/response-cache.models'; +import { SearchQueryResponse } from './search-query-response.model'; +import { SearchFilterConfig } from './search-filter-config.model'; @Component({ template: '' }) class DummyComponent { @@ -40,8 +56,8 @@ describe('SearchService', () => { providers: [ { provide: Router, useValue: router }, { provide: ActivatedRoute, useValue: route }, - { provide: ResponseCacheService, useValue: {} }, - { provide: RequestService, useValue: {} }, + { provide: ResponseCacheService, useValue: getMockResponseCacheService() }, + { provide: RequestService, useValue: getMockRequestService() }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, SearchService @@ -60,6 +76,26 @@ describe('SearchService', () => { let searchService: SearchService; const router = new RouterStub(); const route = new ActivatedRouteStub(); + + const halService = { + /* tslint:disable:no-empty */ + getEndpoint: () => {} + /* tslint:enable:no-empty */ + + }; + + const remoteDataBuildService = { + toRemoteDataObservable: (requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) => { + return Observable.combineLatest(requestEntryObs, + responseCacheObs, payloadObs, (req, res, pay) => { + return { req, res, pay }; + }); + }, + aggregate: (input: Array>>): Observable> => { + return Observable.of(new RemoteData(false, false, true, null, [])); + } + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -74,10 +110,10 @@ describe('SearchService', () => { providers: [ { provide: Router, useValue: router }, { provide: ActivatedRoute, useValue: route }, - { provide: ResponseCacheService, useValue: {} }, - { provide: RequestService, useValue: {} }, - { provide: RemoteDataBuildService, useValue: {} }, - { provide: HALEndpointService, useValue: {} }, + { provide: ResponseCacheService, useValue: getMockResponseCacheService() }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, + { provide: HALEndpointService, useValue: halService }, SearchService ], }); @@ -113,5 +149,95 @@ describe('SearchService', () => { searchService.getViewMode().subscribe((mode) => viewMode = mode); expect(viewMode).toEqual(ViewMode.Grid); }); + + describe('when search is called', () => { + const endPoint = 'http://endpoint.com/test/test'; + const searchOptions = new PaginatedSearchOptions(); + const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] }); + const response = new SearchSuccessResponse(queryResponse, '200'); + const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + beforeEach(() => { + 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 + /* tslint:enable:no-empty */ + }); + + it('should call getEndpoint on the halService', () => { + expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); + }); + + it('should send out the request on the request service', () => { + expect((searchService as any).requestService.configure).toHaveBeenCalled(); + }); + + it('should call getByHref on the request service with the correct request url', () => { + expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint); + }); + it('should call get on the request service with the correct request url', () => { + expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint); + }); + }); + + describe('when getConfig is called without a scope', () => { + const endPoint = 'http://endpoint.com/test/config'; + const filterConfig = [new SearchFilterConfig()]; + const response = new FacetConfigSuccessResponse(filterConfig, '200'); + const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + beforeEach(() => { + 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 + /* tslint:enable:no-empty */ + }); + + it('should call getEndpoint on the halService', () => { + expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); + }); + + it('should send out the request on the request service', () => { + expect((searchService as any).requestService.configure).toHaveBeenCalled(); + }); + + it('should call getByHref on the request service with the correct request url', () => { + expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint); + }); + it('should call get on the request service with the correct request url', () => { + expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint); + }); + }); + + describe('when getConfig is called with a scope', () => { + const endPoint = 'http://endpoint.com/test/config'; + const scope = 'test'; + const requestUrl = endPoint + '?scope=' + scope; + const filterConfig = [new SearchFilterConfig()]; + const response = new FacetConfigSuccessResponse(filterConfig, '200'); + const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + beforeEach(() => { + 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 + /* tslint:enable:no-empty */ + }); + + it('should call getEndpoint on the halService', () => { + expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); + }); + + it('should send out the request on the request service', () => { + expect((searchService as any).requestService.configure).toHaveBeenCalled(); + }); + + it('should call getByHref on the request service with the correct request url', () => { + expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl); + }); + it('should call get on the request service with the correct request url', () => { + expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl); + }); + }); }); }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 42f527a411..6b1034fa33 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -96,6 +96,7 @@ export class SearchService implements OnDestroy { } search(searchOptions?: PaginatedSearchOptions): Observable>>> { + // this.halService.getEndpoint(this.searchLinkPath).subscribe((t) => console.log(t)); const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { if (hasValue(searchOptions)) { @@ -110,7 +111,6 @@ export class SearchService implements OnDestroy { }), tap((request: RestRequest) => this.requestService.configure(request)), ); - const requestEntryObs = requestObs.pipe( flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts index ed8ffa028d..02d3e54282 100644 --- a/src/app/shared/mocks/mock-request.service.ts +++ b/src/app/shared/mocks/mock-request.service.ts @@ -1,8 +1,10 @@ import { RequestService } from '../../core/data/request.service'; +import { RequestEntry } from '../../core/data/request.reducer'; export function getMockRequestService(): RequestService { return jasmine.createSpyObj('requestService', { configure: () => false, - generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78' + generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', + getByHref: (uuid: string) => new RequestEntry() }); } diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/mocks/mock-response-cache.service.ts index 95b4e7aca0..ad1457c3eb 100644 --- a/src/app/shared/mocks/mock-response-cache.service.ts +++ b/src/app/shared/mocks/mock-response-cache.service.ts @@ -1,10 +1,12 @@ import { ResponseCacheService } from '../../core/cache/response-cache.service'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { RestResponse } from '../../core/cache/response-cache.models'; export function getMockResponseCacheService(): ResponseCacheService { - return jasmine.createSpyObj('ResponseCacheService', [ - 'add', - 'get', - 'has', - ]); + return jasmine.createSpyObj('ResponseCacheService', { + add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(), + get: (key: string) => new ResponseCacheEntry(), + has: (key: string) => false, + }); } From e56ea2aff894ea21537d66340b74b8a7b6a59787 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 13 Apr 2018 15:18:35 +0200 Subject: [PATCH 19/34] Applied feedback and other fixes --- resources/i18n/en.json | 3 +- .../search-facet-filter.component.html | 2 +- .../search-facet-filter.component.spec.ts | 7 +-- .../search-facet-filter.component.ts | 19 ++----- .../+search-page/search-page.component.html | 4 +- src/app/+search-page/search-page.component.ts | 2 + .../search-results.component.html | 5 +- .../search-results.component.ts | 3 +- .../search-service/search.service.spec.ts | 16 ++++-- .../search-service/search.service.ts | 54 +++++-------------- .../builders/remote-data-build.service.ts | 7 ++- .../data/base-response-parsing.service.ts | 5 +- .../data/config-response-parsing.service.ts | 2 +- .../core/data/dso-response-parsing.service.ts | 2 +- ...acet-value-map-response-parsing.service.ts | 2 +- .../facet-value-response-parsing.service.ts | 6 +-- src/app/core/data/paginated-list.ts | 36 ++++++++++++- .../data/search-response-parsing.service.ts | 2 +- src/app/core/shared/page-info.model.ts | 11 ++++ src/app/shared/testing/router-stub.ts | 2 + 20 files changed, 107 insertions(+), 83 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 68dfcbe147..5c750b2397 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -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", diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html index 8687aeb25b..074c5700d7 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html @@ -20,7 +20,7 @@
- {{"search.filters.filter.show-more" | translate}} { 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] }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 04258fbd35..bf78bbe5ec 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -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>>> = []; filterValues$: BehaviorSubject = new BehaviorSubject(this.filterValues); currentPage: Observable; + isLastPage$: BehaviorSubject = 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 { - return this.filterValues$.flatMap((map) => { - - if (isNotEmpty(map)) { - return map.pop().map((rd: RemoteData>) => 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(); } } diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index c50bb30696..d53e4776b1 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -36,6 +36,4 @@
- - - + \ No newline at end of file diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index c9efc65676..03f664de59 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -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 { diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index 7d7c169380..ec103e8957 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,10 +1,11 @@ +

{{ 'search.results.head' | translate }}

-

{{ 'search.results.head' | translate }}

- + + \ No newline at end of file diff --git a/src/app/+search-page/search-results/search-results.component.ts b/src/app/+search-page/search-results/search-results.component.ts index 4b3fec4565..14ccb5d541 100644 --- a/src/app/+search-page/search-results/search-results.component.ts +++ b/src/app/+search-page/search-results/search-results.component.ts @@ -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>>; + @Input() searchResults: RemoteData>>; @Input() searchConfig: SearchOptions; @Input() sortConfig: SortOptions; @Input() viewMode: ViewMode; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index ef6efe3deb..4b558f8726 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -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 */ }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 6b1034fa33..ec4ccc9742 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -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; constructor(private router: Router, @@ -96,7 +66,6 @@ export class SearchService implements OnDestroy { } search(searchOptions?: PaginatedSearchOptions): Observable>>> { - // 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>> = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { + return sqr.objects.map((object: NormalizedSearchResult, index: number) => { let co = DSpaceObject; if (dsos.payload[index]) { @@ -150,7 +120,7 @@ export class SearchService implements OnDestroy { } }); }); - + const pageInfoObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), map((response: FacetValueSuccessResponse) => response.pageInfo) @@ -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 { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index af2c80c9aa..ece80cf4ca 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -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(input: Array>>): Observable> { + + if (isEmpty(input)) { + return Observable.of(new RemoteData(false, false, true, null, [])); + } + return Observable.combineLatest( ...input, (...arr: Array>) => { diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 9d6a5851e5..bde0857946 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -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 }); diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 69be4bbc02..033c9ddc68 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -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(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( diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 11590d0431..9651eb3157 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -28,7 +28,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const processRequestDTO = this.process(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)) } } diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index dfd72c0cc5..8588e4aa0b 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -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; }); diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 17f0730566..bc3f4e5368 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -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)); } } diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index 7e4a57f84e..fe095ecc45 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -45,10 +45,44 @@ export class PaginatedList { 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; + } + + + + } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 927c5ff010..707875911d 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -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)); } } diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index 0cb5cac5b1..ba2af24dce 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -28,4 +28,15 @@ export class PageInfo { @autoserializeAs(Number, 'number') currentPage: number; + @autoserialize + last: string; + + @autoserialize + next: string; + + @autoserialize + prev: string; + + @autoserialize + first: string; } diff --git a/src/app/shared/testing/router-stub.ts b/src/app/shared/testing/router-stub.ts index ca4f7642b8..31c09c41e3 100644 --- a/src/app/shared/testing/router-stub.ts +++ b/src/app/shared/testing/router-stub.ts @@ -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; } From 790ca4bc79f1ac8a2718cd90b2b23ebff5d0c5dc Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 13 Apr 2018 15:31:26 +0200 Subject: [PATCH 20/34] Fixed TSLint errors --- src/app/+search-page/search-service/search.service.ts | 2 +- src/app/core/data/paginated-list.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index ec4ccc9742..25b8c0b23e 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -120,7 +120,7 @@ export class SearchService implements OnDestroy { } }); }); - + const pageInfoObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), map((response: FacetValueSuccessResponse) => response.pageInfo) diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index fe095ecc45..21cc13f3fa 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -81,8 +81,4 @@ export class PaginatedList { set last(last: string) { this.pageInfo.last = last; } - - - - } From 60366e3eecec3f4a7d334050582072a9eb0d5061 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 13 Apr 2018 15:35:26 +0200 Subject: [PATCH 21/34] removed log --- src/app/+search-page/search-page.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 03f664de59..c9efc65676 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -65,8 +65,6 @@ 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 { From c125f36f43403c320258bb0ef4571ee3500770bc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 20 Apr 2018 11:17:52 +0200 Subject: [PATCH 22/34] fixed an issue where browse definition REST URLs would be wrong in certain cases --- src/app/core/browse/browse.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index a7b7314d54..2e99dcc0d3 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -36,7 +36,7 @@ export class BrowseService { getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); - return this.halService.getEndpoint(linkPath) + return this.halService.getEndpoint(this.linkPath) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)) From 2317f1f3a30806950e8e26dbd963fe5e030c530c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 20 Apr 2018 14:19:18 +0200 Subject: [PATCH 23/34] fixed an issue where changing the sort direction wouldn't work --- resources/i18n/en.json | 4 ++++ .../search-filter/search-filter.service.ts | 6 ++++-- .../search-settings/search-settings.component.html | 2 +- src/app/core/cache/models/sort-options.model.ts | 6 +++--- src/app/shared/pagination/pagination.component.html | 2 +- src/app/shared/pagination/pagination.component.ts | 2 +- src/app/shared/route.service.ts | 10 +++++----- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 5c750b2397..53ae9015f6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -56,6 +56,10 @@ "detail": "{{ range }} of {{ total }}" } }, + "sorting": { + "ASC": "Ascending", + "DESC": "Descending" + }, "title": "DSpace", "404": { "help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 1ba8182d33..62f906c004 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -10,7 +10,7 @@ import { SearchFilterInitialExpandAction, SearchFilterResetPageAction, SearchFilterToggleAction } from './search-filter.actions'; -import { hasValue, } from '../../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchService } from '../../search-service/search.service'; import { RouteService } from '../../../shared/route.service'; @@ -59,7 +59,9 @@ export class SearchFilterService { getCurrentSort(): Observable { const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); const sortField$ = this.routeService.getQueryParameterValue('sortField'); - return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => new SortOptions(sortField || undefined, SortDirection[sortDirection])); + return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => + new SortOptions(isNotEmpty(sortField) ? sortField : undefined, SortDirection[sortDirection]) + ); } getCurrentFilters() { diff --git a/src/app/+search-page/search-settings/search-settings.component.html b/src/app/+search-page/search-settings/search-settings.component.html index b0d3294e30..18fd45caed 100644 --- a/src/app/+search-page/search-settings/search-settings.component.html +++ b/src/app/+search-page/search-settings/search-settings.component.html @@ -5,7 +5,7 @@ diff --git a/src/app/core/cache/models/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts index 751b72b399..b2380cbaf3 100644 --- a/src/app/core/cache/models/sort-options.model.ts +++ b/src/app/core/cache/models/sort-options.model.ts @@ -1,10 +1,10 @@ export enum SortDirection { - Ascending = 'ASC', - Descending = 'DESC' + ASC = 'ASC', + DESC = 'DESC' } export class SortOptions { - constructor(public field: string = 'dc.title', public direction: SortDirection = SortDirection.Ascending) { + constructor(public field: string = 'dc.title', public direction: SortDirection = SortDirection.ASC) { } } diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 0ad812a6b6..e974bb6eb0 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 6ab0e2a567..faaf20ec79 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -144,7 +144,7 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Direction in which to sort: ascending or descending */ - public sortDirection: SortDirection = SortDirection.Ascending; + public sortDirection: SortDirection = SortDirection.ASC; /** * Name of the field that's used to sort by diff --git a/src/app/shared/route.service.ts b/src/app/shared/route.service.ts index 10a7eaecb9..9c2b64ede1 100644 --- a/src/app/shared/route.service.ts +++ b/src/app/shared/route.service.ts @@ -13,19 +13,19 @@ export class RouteService { } getQueryParameterValues(paramName: string): Observable { - return this.route.queryParamMap.map((map) => [...map.getAll(paramName)]); + return this.route.queryParamMap.map((map) => [...map.getAll(paramName)]).distinctUntilChanged(); } getQueryParameterValue(paramName: string): Observable { - return this.route.queryParamMap.map((map) => map.get(paramName)); + return this.route.queryParamMap.map((map) => map.get(paramName)).distinctUntilChanged(); } hasQueryParam(paramName: string): Observable { - return this.route.queryParamMap.map((map) => map.has(paramName)); + return this.route.queryParamMap.map((map) => map.has(paramName)).distinctUntilChanged(); } hasQueryParamWithValue(paramName: string, paramValue: string): Observable { - return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1); + return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1).distinctUntilChanged(); } getQueryParamsWithPrefix(prefix: string): Observable { @@ -38,6 +38,6 @@ export class RouteService { params[key] = [...map.getAll(key)]; }); return params; - }); + }).distinctUntilChanged(); } } From 545145a75938475da92f6056c25eab33452bb4b4 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 20 Apr 2018 16:01:59 +0200 Subject: [PATCH 24/34] fixed an issue where a search result without hitlighlights would break the grid view --- .../data/search-response-parsing.service.ts | 2 +- .../search-result-list-element.component.ts | 34 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 707875911d..c7456aa2f9 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -26,7 +26,7 @@ export class SearchResponseParsingService implements ResponseParsingService { value: hhObject[key].join('...') })) } else { - return undefined; + return []; } }); diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 31e31c72a8..fd821997ad 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -1,12 +1,12 @@ import { Component, Inject } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; import { SearchResult } from '../../../+search-page/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { Metadatum } from '../../../core/shared/metadatum.model'; -import { isEmpty, hasNoValue, isNotEmpty } from '../../empty.util'; +import { hasNoValue, isEmpty } from '../../empty.util'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; -import { Observable } from 'rxjs/Observable'; import { TruncatableService } from '../../truncatable/truncatable.service'; @Component({ @@ -24,15 +24,13 @@ export class SearchResultListElementComponent, K exten getValues(keys: string[]): string[] { const results: string[] = new Array(); - if (isNotEmpty(this.object.hitHighlights)) { - this.object.hitHighlights.forEach( - (md: Metadatum) => { - if (keys.indexOf(md.key) > -1) { - results.push(md.value); - } + this.object.hitHighlights.forEach( + (md: Metadatum) => { + if (keys.indexOf(md.key) > -1) { + results.push(md.value); } - ); - } + } + ); if (isEmpty(results)) { this.dso.filterMetadata(keys).forEach( (md: Metadatum) => { @@ -45,16 +43,14 @@ export class SearchResultListElementComponent, K exten getFirstValue(key: string): string { let result: string; - if (isNotEmpty(this.object.hitHighlights)) { - this.object.hitHighlights.some( - (md: Metadatum) => { - if (key === md.key) { - result = md.value; - return true; - } + this.object.hitHighlights.some( + (md: Metadatum) => { + if (key === md.key) { + result = md.value; + return true; } - ); - } + } + ); if (hasNoValue(result)) { result = this.dso.findMetadata(key); } From 994c0899255d2602c25a8633b2276b511328068a Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 10:57:12 +0200 Subject: [PATCH 25/34] fixed a few sorting, rpp and view persistence issues with grid view --- nodemon.json | 3 +- .../search-filter/search-filter.component.ts | 3 +- .../search-filter/search-filter.service.ts | 30 ++++---- .../search-filters.component.ts | 2 + .../+search-page/search-page.component.html | 4 +- .../search-page.component.spec.ts | 3 +- src/app/+search-page/search-page.component.ts | 1 - src/app/+search-page/search-page.module.ts | 2 + .../search-results.component.html | 4 +- .../search-service/search.service.ts | 3 +- .../search-settings.component.ts | 6 +- src/app/shared/host-window.service.ts | 72 +++++++++++++------ .../collection-grid-element.component.html | 8 +-- .../community-grid-element.component.html | 8 +-- .../item-grid-element.component.html | 4 +- .../object-grid/object-grid.component.html | 8 ++- .../object-grid/object-grid.component.scss | 37 +++++++--- .../object-grid/object-grid.component.ts | 64 +++++++++++++++-- ...-search-result-grid-element.component.html | 8 +-- ...-search-result-grid-element.component.html | 8 +-- .../search-form/search-form.component.spec.ts | 1 - .../search-form/search-form.component.ts | 22 +++--- webpack/webpack.common.js | 3 + 23 files changed, 208 insertions(+), 96 deletions(-) diff --git a/nodemon.json b/nodemon.json index 00313fe368..e76cdba1a4 100644 --- a/nodemon.json +++ b/nodemon.json @@ -4,5 +4,6 @@ "config", "src/index.html" ], - "ext": "js ts json html" + "ext": "js ts json html", + "delay": "500" } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index 90d3b50786..be26075d25 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -28,14 +28,13 @@ export class SearchFilterComponent implements OnInit { } ngOnInit() { - const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => { + this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => { if (this.filter.isOpenByDefault || isActive) { this.initialExpand(); } else { this.initialCollapse(); } }); - sub.unsubscribe(); } toggle() { diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 62f906c004..cbe6d79dfc 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; @@ -79,20 +80,21 @@ export class SearchFilterService { this.getCurrentView(), this.getCurrentScope(), this.getCurrentQuery(), - this.getCurrentFilters(), - (pagination, sort, view, scope, query, filters) => { - return Object.assign(new PaginatedSearchOptions(), - defaults, - { - pagination: pagination, - sort: sort, - view: view, - scope: scope || defaults.scope, - query: query, - filters: filters - }) - } - ) + this.getCurrentFilters()).pipe( + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(([pagination, sort, view, scope, query, filters]) => { + return Object.assign(new PaginatedSearchOptions(), + defaults, + { + pagination: pagination, + sort: sort, + view: view, + scope: scope || defaults.scope, + query: query, + filters: filters + }) + }) + ) } getSearchOptions(defaults: any = {}): Observable { diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index 517b2e1e59..cb2dae0290 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -23,6 +23,8 @@ export class SearchFiltersComponent { constructor(private searchService: SearchService, private filterService: SearchFilterService) { this.filters = searchService.getConfig(); this.clearParams = filterService.getCurrentFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;}); + + this.filters.subscribe((v) => console.log('this.filters', v)); } getSearchLink() { diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index d53e4776b1..d911a4cb9b 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -7,7 +7,7 @@
@@ -36,4 +36,4 @@
- \ No newline at end of file + diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index cc53e18871..c51547a53d 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -38,7 +38,8 @@ describe('SearchPageComponent', () => { const sort: SortOptions = new SortOptions(); const mockResults = Observable.of(['test', 'data']); const searchServiceStub = jasmine.createSpyObj('SearchService', { - search: mockResults + search: mockResults, + getSearchLink: '/search' }); const queryParam = 'test query'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index c9efc65676..6b2d9b95eb 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -31,7 +31,6 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; export class SearchPageComponent implements OnInit { resultsRD$: Observable>>>; - currentParams = {}; searchOptions$: Observable; sortConfig: SortOptions; scopeListRD$: Observable>>; diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 7c2001c909..1468fe532e 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; import { SearchPageRoutingModule } from './search-page-routing.module'; import { SearchPageComponent } from './search-page.component'; @@ -31,6 +32,7 @@ const effects = [ CommonModule, SharedModule, EffectsModule.forFeature(effects), + CoreModule.forRoot() ], declarations: [ SearchPageComponent, diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index ec103e8957..ed6fc18d9c 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,5 +1,5 @@

{{ 'search.results.head' | translate }}

-
+
- \ No newline at end of file + diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 25b8c0b23e..07546c062c 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -167,7 +167,8 @@ export class SearchService implements OnDestroy { // get search results from response cache const facetConfigObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), - map((response: FacetConfigSuccessResponse) => response.results) + map((response: FacetConfigSuccessResponse) => + response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result))) ); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index cc22da7176..145b58e27b 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -22,8 +22,6 @@ export class SearchSettingsComponent implements OnInit { */ public pageSize; @Input() public pageSizeOptions; - public listPageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100]; - public gridPageSizeOptions: number[] = [12, 24, 36, 48 , 50, 62, 74, 84]; private sub; private scope: string; @@ -51,9 +49,9 @@ export class SearchSettingsComponent implements OnInit { this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize; this.direction = params.sortDirection || this.searchOptions.sort.direction; if (params.view === ViewMode.Grid) { - this.pageSizeOptions = this.gridPageSizeOptions; + this.pageSizeOptions = this.pageSizeOptions; } else { - this.pageSizeOptions = this.listPageSizeOptions; + this.pageSizeOptions = this.pageSizeOptions; } }); } diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts index 6fa5a6b32b..13ecbe7538 100644 --- a/src/app/shared/host-window.service.ts +++ b/src/app/shared/host-window.service.ts @@ -1,3 +1,4 @@ +import { distinctUntilChanged, map } from 'rxjs/operators'; import { HostWindowState } from './host-window.reducer'; import { Injectable } from '@angular/core'; import { createSelector, Store } from '@ngrx/store'; @@ -8,11 +9,18 @@ import { AppState } from '../app.reducer'; // TODO: ideally we should get these from sass somehow export enum GridBreakpoint { - XS = 0, - SM = 576, - MD = 768, - LG = 992, - XL = 1200 + SM_MIN = 576, + MD_MIN = 768, + LG_MIN = 992, + XL_MIN = 1200 +} + +export enum WidthCategory { + XS, + SM, + MD, + LG, + XL } const hostWindowStateSelector = (state: AppState) => state.hostWindow; @@ -31,33 +39,57 @@ export class HostWindowService { .filter((width) => hasValue(width)); } + get widthCategory(): Observable { + return this.getWidthObs().pipe( + map((width: number) => { + if (width < GridBreakpoint.SM_MIN) { + return WidthCategory.XS + } else if (width >= GridBreakpoint.SM_MIN && width < GridBreakpoint.MD_MIN) { + return WidthCategory.SM + } else if (width >= GridBreakpoint.MD_MIN && width < GridBreakpoint.LG_MIN) { + return WidthCategory.MD + } else if (width >= GridBreakpoint.LG_MIN && width < GridBreakpoint.XL_MIN) { + return WidthCategory.LG + } else { + return WidthCategory.XL + } + }), + distinctUntilChanged() + ); + } + isXs(): Observable { - return this.getWidthObs() - .map((width) => width < GridBreakpoint.SM) - .distinctUntilChanged(); + return this.widthCategory.pipe( + map((widthCat: WidthCategory) => widthCat === WidthCategory.XS), + distinctUntilChanged() + ); } isSm(): Observable { - return this.getWidthObs() - .map((width) => width >= GridBreakpoint.SM && width < GridBreakpoint.MD) - .distinctUntilChanged(); + return this.widthCategory.pipe( + map((widthCat: WidthCategory) => widthCat === WidthCategory.SM), + distinctUntilChanged() + ); } isMd(): Observable { - return this.getWidthObs() - .map((width) => width >= GridBreakpoint.MD && width < GridBreakpoint.LG) - .distinctUntilChanged(); + return this.widthCategory.pipe( + map((widthCat: WidthCategory) => widthCat === WidthCategory.MD), + distinctUntilChanged() + ); } isLg(): Observable { - return this.getWidthObs() - .map((width) => width >= GridBreakpoint.LG && width < GridBreakpoint.XL) - .distinctUntilChanged(); + return this.widthCategory.pipe( + map((widthCat: WidthCategory) => widthCat === WidthCategory.LG), + distinctUntilChanged() + ); } isXl(): Observable { - return this.getWidthObs() - .map((width) => width >= GridBreakpoint.XL) - .distinctUntilChanged(); + return this.widthCategory.pipe( + map((widthCat: WidthCategory) => widthCat === WidthCategory.XL), + distinctUntilChanged() + ); } } diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html index b1287212a3..9fecb51b9a 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html @@ -1,13 +1,13 @@
- - - + + +

{{object.name}}

{{object.shortDescription}}

- View + View
diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html index b6f4c5c5d9..31a9e8ad3d 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html @@ -1,14 +1,14 @@
- - - + + +

{{object.name}}

{{object.shortDescription}}

- View + View
diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html index 328bfc3bc9..cc2f2efdb1 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html @@ -1,6 +1,6 @@
- + @@ -16,7 +16,7 @@

{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}

- View + View
diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index fcf3a42662..18aa2dd2e7 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -10,12 +10,14 @@ (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" (paginationChange)="onPaginationChange($event)"> -
-
+
+
+
+
+ diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 1b9418be48..33b158d971 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -11,14 +11,33 @@ ds-wrapper-grid-element ::ng-deep { } } +$gutter: ($grid-gutter-width / 2); +$min-width: 300px; +$max-cols: 3; + .card-columns { - @include media-breakpoint-only(lg) { - column-count: 3; + display: flex; + flex-wrap: wrap; + margin-left: -$gutter; + margin-top: -$gutter; + + .card-column { + flex: 1 0 $min-width; + margin-left: $gutter; + margin-top: $gutter; + + @for $i from 2 through $max-cols { + $screen-width: ($min-width*$i)+($gutter*$i); + $column-width: (100%/$i); + @media (min-width: $screen-width) { + max-width: calc(#{$column-width} - #{$gutter}); + } + } + + $column-width: (100%/$max-cols); + @media (min-width: $min-width*$max-cols) { + min-width: calc(#{$column-width} - #{$gutter}); + } } - @include media-breakpoint-only(sm) { - column-count: 2; - } - @include media-breakpoint-only(xs) { - column-count: 1; - } -} \ No newline at end of file +} + diff --git a/src/app/shared/object-grid/object-grid.component.ts b/src/app/shared/object-grid/object-grid.component.ts index a8f8ebb183..bffa79b62b 100644 --- a/src/app/shared/object-grid/object-grid.component.ts +++ b/src/app/shared/object-grid/object-grid.component.ts @@ -2,16 +2,21 @@ import { ChangeDetectionStrategy, Component, EventEmitter, - Input, + Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; import { RemoteData } from '../../core/data/remote-data'; import { fadeIn } from '../animations/fade'; +import { hasNoValue, hasValue } from '../empty.util'; +import { HostWindowService, WidthCategory } from '../host-window.service'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; @@ -25,18 +30,18 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o animations: [fadeIn] }) -export class ObjectGridComponent { +export class ObjectGridComponent implements OnInit { @Input() config: PaginationComponentOptions; @Input() sortConfig: SortOptions; @Input() hideGear = false; @Input() hidePagerWhenSinglePage = true; - private _objects: RemoteData>; + private _objects$: BehaviorSubject>>; @Input() set objects(objects: RemoteData>) { - this._objects = objects; + this._objects$.next(objects); } get objects() { - return this._objects; + return this._objects$.getValue(); } /** @@ -77,6 +82,55 @@ export class ObjectGridComponent { */ @Output() sortFieldChange: EventEmitter = new EventEmitter(); data: any = {}; + columns$: Observable + + constructor(private hostWindow: HostWindowService) { + this._objects$ = new BehaviorSubject(undefined); + } + + ngOnInit(): void { + const nbColumns$ = this.hostWindow.widthCategory.pipe( + map((widthCat: WidthCategory) => { + switch (widthCat) { + case WidthCategory.XL: + case WidthCategory.LG: { + return 3; + } + case WidthCategory.MD: + case WidthCategory.SM: { + return 2; + } + default: { + return 1; + } + } + }) + ).startWith(3); + + this.columns$ = Observable.combineLatest( + nbColumns$, + this._objects$, + (nbColumns, objects) => { + if (hasValue(objects) && hasValue(objects.payload) && hasValue(objects.payload.page)) { + const page = objects.payload.page; + + const result = []; + + page.forEach((obj: ListableObject, i: number) => { + const colNb = i % nbColumns; + let col = result[colNb]; + if (hasNoValue(col)) { + col = []; + } + result[colNb] = [...col, obj]; + }); + return result; + } else { + return []; + } + }); + } + onPageChange(event) { this.pageChange.emit(event); } diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html index d6b1bfb5f4..91548d945d 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html @@ -1,13 +1,13 @@
- - - + + +

{{dso.name}}

{{dso.shortDescription}}

- View + View
diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html index 8ff6874bff..95094a6fa1 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html @@ -1,14 +1,14 @@
- - - + + +

{{dso.name}}

{{dso.shortDescription}}

- View + View
diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index d148429b01..30f5801cc2 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { SearchFormComponent } from './search-form.component'; -import { Observable } from 'rxjs/Observable'; import { FormsModule } from '@angular/forms'; import { ResourceType } from '../../core/shared/resource-type'; import { RouterTestingModule } from '@angular/router/testing'; diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 49f825e28c..ed7572f94d 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Router } from '@angular/router'; import { isNotEmpty, hasValue, isEmpty } from '../empty.util'; @@ -17,8 +18,7 @@ import { isNotEmpty, hasValue, isEmpty } from '../empty.util'; export class SearchFormComponent { @Input() query: string; selectedId = ''; - // Optional existing search parameters - @Input() currentParams: {}; + @Input() currentUrl: string; @Input() scopes: DSpaceObject[]; @Input() @@ -34,16 +34,14 @@ export class SearchFormComponent { } updateSearch(data: any) { - this.router.navigate(['/search'], { - queryParams: Object.assign({}, this.currentParams, - { - query: data.query, - scope: data.scope || undefined, - page: data.page || 1 - } - ) - }) - ; + this.router.navigate([this.currentUrl], { + queryParams: { + query: data.query, + scope: data.scope || undefined, + page: data.page || 1 + }, + queryParamsHandling: 'merge' + }); } isNotEmpty(object: any) { diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 6bf4620ca7..904e52e0ff 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -13,6 +13,9 @@ module.exports = { output: { path: root('dist') }, + watchOptions: { + aggregateTimeout: 500, + }, module: { rules: [{ test: /\.ts$/, From 2d0a43f91f480e996d03487cab4fd568a5642c54 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 11:38:36 +0200 Subject: [PATCH 26/34] added tests for new HostWindowService code --- nodemon.json | 2 +- src/app/shared/host-window.service.spec.ts | 75 +++++++++++++++++++++- webpack/webpack.common.js | 2 +- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/nodemon.json b/nodemon.json index e76cdba1a4..107ae1a754 100644 --- a/nodemon.json +++ b/nodemon.json @@ -5,5 +5,5 @@ "src/index.html" ], "ext": "js ts json html", - "delay": "500" + "delay": "50" } diff --git a/src/app/shared/host-window.service.spec.ts b/src/app/shared/host-window.service.spec.ts index 674d0e1332..41be3211e9 100644 --- a/src/app/shared/host-window.service.spec.ts +++ b/src/app/shared/host-window.service.spec.ts @@ -1,9 +1,10 @@ import { Store } from '@ngrx/store'; +import { cold, hot } from 'jasmine-marbles'; import { Observable } from 'rxjs/Observable'; import { AppState } from '../app.reducer'; import { HostWindowState } from './host-window.reducer'; -import { HostWindowService } from './host-window.service'; +import { GridBreakpoint, HostWindowService, WidthCategory } from './host-window.service'; describe('HostWindowService', () => { let service: HostWindowService; @@ -189,4 +190,76 @@ describe('HostWindowService', () => { }); }); + describe('widthCategory', () => { + beforeEach(() => { + service = new HostWindowService({} as Store); + }); + + it('should call getWithObs to get the current width', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { a: GridBreakpoint.SM_MIN - 1 })); + + const result = service.widthCategory; + + expect((service as any).getWidthObs).toHaveBeenCalled(); + }); + + it('should return XS if width < SM_MIN', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { a: GridBreakpoint.SM_MIN - 1 })); + + const result = service.widthCategory; + + const expected = cold('b-', { b: WidthCategory.XS }); + expect(result).toBeObservable(expected); + }); + + it('should return SM if SM_MIN <= width < MD_MIN', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { + a: GridBreakpoint.SM_MIN + Math.floor((GridBreakpoint.MD_MIN - GridBreakpoint.SM_MIN) / 2) + })); + + const result = service.widthCategory; + + const expected = cold('b-', { b: WidthCategory.SM }); + expect(result).toBeObservable(expected); + }); + + it('should return MD if MD_MIN <= width < LG_MIN', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { + a: GridBreakpoint.MD_MIN + Math.floor((GridBreakpoint.LG_MIN - GridBreakpoint.MD_MIN) / 2) + })); + + const result = service.widthCategory; + + const expected = cold('b-', { b: WidthCategory.MD }); + expect(result).toBeObservable(expected); + }); + + it('should return LG if LG_MIN <= width < XL_MIN', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { + a: GridBreakpoint.LG_MIN + Math.floor((GridBreakpoint.XL_MIN - GridBreakpoint.LG_MIN) / 2) + })); + + const result = service.widthCategory; + + const expected = cold('b-', { b: WidthCategory.LG }); + expect(result).toBeObservable(expected); + }); + + it('should return XL if width >= XL_MIN', () => { + spyOn(service as any, 'getWidthObs').and + .returnValue(hot('a-', { a: GridBreakpoint.XL_MIN + 1 })); + + const result = service.widthCategory; + + const expected = cold('b-', { b: WidthCategory.XL }); + expect(result).toBeObservable(expected); + }); + + }); + }); diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 904e52e0ff..3cbfe5c648 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -14,7 +14,7 @@ module.exports = { path: root('dist') }, watchOptions: { - aggregateTimeout: 500, + aggregateTimeout: 50, }, module: { rules: [{ From cb4c062ad9c843520cc75cbdd0c862ab51389c2d Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 11:58:32 +0200 Subject: [PATCH 27/34] fixed an issue with the AoT build --- src/app/+search-page/search-page.component.html | 2 +- src/app/+search-page/search-page.component.ts | 4 ++++ src/app/+search-page/search-service/search.service.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index d911a4cb9b..1a1f379920 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -7,7 +7,7 @@
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 6b2d9b95eb..95ff8f498f 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -77,4 +77,8 @@ export class SearchPageComponent implements OnInit { public isSidebarCollapsed(): Observable { return this.sidebarService.isCollapsed; } + + public getSearchLink(): string { + return this.service.getSearchLink(); + } } diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 07546c062c..2e1a6d5a6d 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -236,7 +236,7 @@ export class SearchService implements OnDestroy { this.router.navigate([this.getSearchLink()], navigationExtras); } - getSearchLink() { + getSearchLink(): string { const urlTree = this.router.parseUrl(this.router.url); const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; return '/' + g.toString(); From 5e2dc51828fff03922f7511fb7ec59bdb8a8926a Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 13:27:15 +0200 Subject: [PATCH 28/34] replaced css with bootstrap classes --- .../object-grid/object-grid.component.html | 4 +-- .../object-grid/object-grid.component.scss | 33 +++++-------------- .../object-grid/object-grid.component.ts | 5 +-- ...-search-result-grid-element.component.html | 4 +-- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 18aa2dd2e7..9d1f8f5ea2 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -10,8 +10,8 @@ (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" (paginationChange)="onPaginationChange($event)"> -
-
+
+
diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 33b158d971..ff78634863 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -1,43 +1,26 @@ @import '../../../styles/variables'; @import '../../../styles/mixins'; +$ds-wrapper-grid-spacing: $spacer/2; + ds-wrapper-grid-element ::ng-deep { div.thumbnail > img { height: $card-thumbnail-height; width: 100%; } div.card { - margin-bottom: $spacer; + margin-top: $ds-wrapper-grid-spacing; + margin-bottom: $ds-wrapper-grid-spacing; } } -$gutter: ($grid-gutter-width / 2); -$min-width: 300px; -$max-cols: 3; - .card-columns { - display: flex; - flex-wrap: wrap; - margin-left: -$gutter; - margin-top: -$gutter; + margin-left: -$ds-wrapper-grid-spacing; + margin-right: -$ds-wrapper-grid-spacing; .card-column { - flex: 1 0 $min-width; - margin-left: $gutter; - margin-top: $gutter; - - @for $i from 2 through $max-cols { - $screen-width: ($min-width*$i)+($gutter*$i); - $column-width: (100%/$i); - @media (min-width: $screen-width) { - max-width: calc(#{$column-width} - #{$gutter}); - } - } - - $column-width: (100%/$max-cols); - @media (min-width: $min-width*$max-cols) { - min-width: calc(#{$column-width} - #{$gutter}); - } + padding-left: $ds-wrapper-grid-spacing; + padding-right: $ds-wrapper-grid-spacing; } } diff --git a/src/app/shared/object-grid/object-grid.component.ts b/src/app/shared/object-grid/object-grid.component.ts index bffa79b62b..4c4add9b06 100644 --- a/src/app/shared/object-grid/object-grid.component.ts +++ b/src/app/shared/object-grid/object-grid.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; -import { map } from 'rxjs/operators'; +import { distinctUntilChanged, map } from 'rxjs/operators'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -104,7 +104,8 @@ export class ObjectGridComponent implements OnInit { return 1; } } - }) + }), + distinctUntilChanged() ).startWith(3); this.columns$ = Observable.combineLatest( diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index b185caa18f..1cf14587ad 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -1,5 +1,5 @@ - - \ No newline at end of file + From 5ad190c9060cc504a69e46ca116a98d2b4c20dc2 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 13:37:14 +0200 Subject: [PATCH 29/34] fixed an issue where the search form wouldn't work on any page but the search page --- src/app/shared/search-form/search-form.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index ed7572f94d..5fd984a731 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Router } from '@angular/router'; -import { isNotEmpty, hasValue, isEmpty } from '../empty.util'; +import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util'; /** * This component renders a simple item page. @@ -34,7 +34,8 @@ export class SearchFormComponent { } updateSearch(data: any) { - this.router.navigate([this.currentUrl], { + const newUrl = hasValue(this.currentUrl) ? this.currentUrl : 'search'; + this.router.navigate([newUrl], { queryParams: { query: data.query, scope: data.scope || undefined, From d6fc8aca4fd29841387e8b776ee2ed6e5da2d0bd Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 14:36:19 +0200 Subject: [PATCH 30/34] added support for Object.entries, removed a console.log and fixed an issue where the page wouldn't reset after selecting a new filter --- .../search-facet-filter.component.spec.ts | 4 ++-- .../search-facet-filter.component.ts | 10 ++++++++-- .../search-filters/search-filters.component.ts | 2 -- src/app/+search-page/search-options.model.ts | 1 + tsconfig.json | 3 ++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index f91332929e..03b760318f 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -124,14 +124,14 @@ describe('SearchFacetFilterComponent', () => { describe('when the getAddParams method is called wih a 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] }); + expect(result[mockFilterConfig.paramName]).toEqual([value1, value2, value3]); }); }); describe('when the getRemoveParams method is called wih a value', () => { it('should return the selectedValue list with the parameter value left out', () => { const result = comp.getRemoveParams(value1); - expect(result).toEqual({ [mockFilterConfig.paramName]: [value2] }); + expect(result[mockFilterConfig.paramName]).toEqual([value2]); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index bf78bbe5ec..5f8111c87b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -100,11 +100,17 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { return hasValue(o); } getRemoveParams(value: string) { - return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) }; + return { + [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value), + page: 1 + }; } getAddParams(value: string) { - return { [this.filterConfig.paramName]: [...this.selectedValues, value] }; + return { + [this.filterConfig.paramName]: [...this.selectedValues, value], + page: 1 + }; } ngOnDestroy(): void { diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index cb2dae0290..517b2e1e59 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -23,8 +23,6 @@ export class SearchFiltersComponent { constructor(private searchService: SearchService, private filterService: SearchFilterService) { this.filters = searchService.getConfig(); this.clearParams = filterService.getCurrentFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;}); - - this.filters.subscribe((v) => console.log('this.filters', v)); } getSearchLink() { diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts index 4164321680..df8d8e713a 100644 --- a/src/app/+search-page/search-options.model.ts +++ b/src/app/+search-page/search-options.model.ts @@ -1,5 +1,6 @@ import { isNotEmpty } from '../shared/empty.util'; import { URLCombiner } from '../core/url-combiner/url-combiner'; +import 'core-js/fn/object/entries'; export enum ViewMode { List = 'list', diff --git a/tsconfig.json b/tsconfig.json index 8037c659ed..8ab72a4327 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "es6", "es2015", "es2016", - "es2017" + "es2017", + "es2017.object" ] }, "exclude": [ From ebbe2e6a93bc6ee2cb9f8e2fbb327b22e3614a20 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 26 Apr 2018 15:51:36 +0200 Subject: [PATCH 31/34] added tests for the new object-grid-component onInit code --- .../object-grid/object-grid.component.spec.ts | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/src/app/shared/object-grid/object-grid.component.spec.ts b/src/app/shared/object-grid/object-grid.component.spec.ts index e69de29bb2..2ced28718a 100644 --- a/src/app/shared/object-grid/object-grid.component.spec.ts +++ b/src/app/shared/object-grid/object-grid.component.spec.ts @@ -0,0 +1,224 @@ +import { cold, hot } from 'jasmine-marbles'; +import { map } from 'rxjs/operators'; +import { WidthCategory } from '../host-window.service'; +import { ObjectGridComponent } from './object-grid.component'; + +describe('ObjectGridComponent', () => { + const testObjects = [ + { one: 1 }, + { two: 2 }, + { three: 3 }, + { four: 4 }, + { five: 5 }, + { six: 6 }, + { seven: 7 }, + { eight: 8 }, + { nine: 9 }, + { ten: 10 } + ]; + const mockRD = { + payload: { + page: testObjects + } + } as any; + + describe('the number of columns', () => { + + it('should be 3 for xl screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.XL }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: 3 }); + + const result = comp.columns$.pipe( + map((columns) => columns.length) + ); + + expect(result).toBeObservable(expected); + }); + + it('should be 3 for lg screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.LG }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: 3 }); + + const result = comp.columns$.pipe( + map((columns) => columns.length) + ); + + expect(result).toBeObservable(expected); + }); + + it('should be 2 for md screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.MD }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: 2 }); + + const result = comp.columns$.pipe( + map((columns) => columns.length) + ); + + expect(result).toBeObservable(expected); + }); + + it('should be 2 for sm screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.SM }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: 2 }); + + const result = comp.columns$.pipe( + map((columns) => columns.length) + ); + + expect(result).toBeObservable(expected); + }); + + it('should be 1 for xs screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.XS }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: 1 }); + + const result = comp.columns$.pipe( + map((columns) => columns.length) + ); + + expect(result).toBeObservable(expected); + }); + + }); + + describe('The ordering of the content', () => { + it('should be left to right for XL screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.XL }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: [ + [testObjects[0], testObjects[3], testObjects[6], testObjects[9]], + [testObjects[1], testObjects[4], testObjects[7]], + [testObjects[2], testObjects[5], testObjects[8]] + ] }); + + const result = comp.columns$; + + expect(result).toBeObservable(expected); + }); + + it('should be left to right for LG screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.LG }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: [ + [testObjects[0], testObjects[3], testObjects[6], testObjects[9]], + [testObjects[1], testObjects[4], testObjects[7]], + [testObjects[2], testObjects[5], testObjects[8]] + ] }); + + const result = comp.columns$; + + expect(result).toBeObservable(expected); + }); + + it('should be left to right for MD screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.MD }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: [ + [testObjects[0], testObjects[2], testObjects[4], testObjects[6], testObjects[8]], + [testObjects[1], testObjects[3], testObjects[5], testObjects[7], testObjects[9]], + ] }); + + const result = comp.columns$; + + expect(result).toBeObservable(expected); + }); + + it('should be left to right for SM screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.SM }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: [ + [testObjects[0], testObjects[2], testObjects[4], testObjects[6], testObjects[8]], + [testObjects[1], testObjects[3], testObjects[5], testObjects[7], testObjects[9]], + ] }); + + const result = comp.columns$; + + expect(result).toBeObservable(expected); + }); + + it('should be top to bottom for XS screens', () => { + const hostWindowService = { + widthCategory: hot('a', { a: WidthCategory.XS }), + } as any; + const comp = new ObjectGridComponent(hostWindowService); + + (comp as any)._objects$ = hot('b', { b: mockRD }); + + comp.ngOnInit(); + + const expected = cold('c', { c: [ testObjects ] }); + + const result = comp.columns$; + + expect(result).toBeObservable(expected); + }); + }); +}); From 38efd5b542423e8ccb3e768659ac520c3647da08 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 3 May 2018 09:32:48 +0200 Subject: [PATCH 32/34] changed default sort options --- .../+collection-page/collection-page.component.ts | 4 ++-- .../top-level-community-list.component.ts | 4 ++-- .../search-filter/search-filter.service.ts | 12 ++++++++---- src/app/+search-page/search-page.component.ts | 3 ++- .../+search-page/search-service/search.service.ts | 4 ++-- src/app/core/cache/models/sort-options.model.ts | 2 +- .../shared/pagination/pagination.component.spec.ts | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index de7e9a72d4..4a935b73b9 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; -import { SortOptions } from '../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CollectionDataService } from '../core/data/collection-data.service'; import { ItemDataService } from '../core/data/item-data.service'; import { PaginatedList } from '../core/data/paginated-list'; @@ -48,7 +48,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy { this.paginationConfig.id = 'collection-page-pagination'; this.paginationConfig.pageSize = 5; this.paginationConfig.currentPage = 1; - this.sortConfig = new SortOptions(); + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); } ngOnInit(): void { diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 1b71220382..8fca66ea79 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -27,7 +27,7 @@ export class TopLevelCommunityListComponent { this.config.id = 'top-level-pagination'; this.config.pageSize = 5; this.config.currentPage = 1; - this.sortConfig = new SortOptions(); + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.updatePage({ page: this.config.currentPage, diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index cbe6d79dfc..890b32b2f0 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -60,8 +60,12 @@ export class SearchFilterService { getCurrentSort(): Observable { const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); const sortField$ = this.routeService.getQueryParameterValue('sortField'); - return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => - new SortOptions(isNotEmpty(sortField) ? sortField : undefined, SortDirection[sortDirection]) + return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => { + if (isNotEmpty(sortField)) { + const direction = SortDirection[sortDirection]; + return new SortOptions(sortField, direction ? direction : SortDirection.ASC) + } + } ); } @@ -87,7 +91,7 @@ export class SearchFilterService { defaults, { pagination: pagination, - sort: sort, + sort: sort || defaults.sort, view: view, scope: scope || defaults.scope, query: query, @@ -108,7 +112,7 @@ export class SearchFilterService { defaults, { view: view, - scope: scope, + scope: scope || defaults.scope, query: query, filters: filters }) diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 95ff8f498f..4f50723ced 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { flatMap, } from 'rxjs/operators'; -import { SortOptions } from '../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CommunityDataService } from '../core/data/community-data.service'; import { PaginatedList } from '../core/data/paginated-list'; import { RemoteData } from '../core/data/remote-data'; @@ -42,6 +42,7 @@ export class SearchPageComponent implements OnInit { id: 'search-results-pagination', pageSize: 10 }, + sort: new SortOptions('score', SortDirection.DESC), query: '', scope: '' }; diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 2e1a6d5a6d..0351a9a54c 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -7,7 +7,7 @@ import { Observable } from 'rxjs/Observable'; import { flatMap, map, tap } from 'rxjs/operators'; import { ViewMode } from '../../+search-page/search-options.model'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { FacetConfigSuccessResponse, FacetValueSuccessResponse, @@ -61,7 +61,7 @@ export class SearchService implements OnDestroy { pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; - const sort: SortOptions = new SortOptions(); + const sort: SortOptions = new SortOptions('score', SortDirection.DESC); this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort }); } diff --git a/src/app/core/cache/models/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts index b2380cbaf3..247504a63a 100644 --- a/src/app/core/cache/models/sort-options.model.ts +++ b/src/app/core/cache/models/sort-options.model.ts @@ -4,7 +4,7 @@ export enum SortDirection { } export class SortOptions { - constructor(public field: string = 'dc.title', public direction: SortDirection = SortDirection.ASC) { + constructor(public field: string, public direction: SortDirection) { } } diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index ad05f0cdfe..48767cf582 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -41,7 +41,7 @@ import { MockRouter } from '../mocks/mock-router'; import { HostWindowService } from '../host-window.service'; import { EnumKeysPipe } from '../utils/enum-keys-pipe'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { GLOBAL_CONFIG, ENV_CONFIG } from '../../../config'; @@ -349,7 +349,7 @@ class TestComponent { collection: string[] = []; collectionSize: number; paginationOptions = new PaginationComponentOptions(); - sortOptions = new SortOptions(); + sortOptions = new SortOptions('dc.title', SortDirection.ASC); constructor() { this.collection = Array.from(new Array(100), (x, i) => `item ${i + 1}`); From a4b2229509be2eaf47aafe6c1eeebbdcc3499a49 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 3 May 2018 09:48:34 +0200 Subject: [PATCH 33/34] fixed prod build issue --- src/app/+search-page/search-page.component.spec.ts | 4 +--- .../search-settings/search-settings.component.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index c51547a53d..51c3a452e4 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -8,10 +8,8 @@ import { cold, hot } from 'jasmine-marbles'; import { Observable } from 'rxjs/Observable'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CommunityDataService } from '../core/data/community-data.service'; -import { Community } from '../core/shared/community.model'; import { HostWindowService } from '../shared/host-window.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { PaginatedSearchOptions } from './paginated-search-options.model'; import { SearchPageComponent } from './search-page.component'; import { SearchService } from './search-service/search.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -35,7 +33,7 @@ describe('SearchPageComponent', () => { pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; - const sort: SortOptions = new SortOptions(); + const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const mockResults = Observable.of(['test', 'data']); const searchServiceStub = jasmine.createSpyObj('SearchService', { search: mockResults, diff --git a/src/app/+search-page/search-settings/search-settings.component.spec.ts b/src/app/+search-page/search-settings/search-settings.component.spec.ts index 504bfbc2bf..2330b62669 100644 --- a/src/app/+search-page/search-settings/search-settings.component.spec.ts +++ b/src/app/+search-page/search-settings/search-settings.component.spec.ts @@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchSettingsComponent } from './search-settings.component'; import { Observable } from 'rxjs/Observable'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; import { ActivatedRoute } from '@angular/router'; @@ -22,7 +22,7 @@ describe('SearchSettingsComponent', () => { pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; - const sort: SortOptions = new SortOptions(); + const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const mockResults = [ 'test', 'data' ]; const searchServiceStub = { searchOptions: { pagination: pagination, sort: sort }, From b065df356aa45516fdfb3bfc88764e4c47252c19 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 3 May 2018 10:21:31 +0200 Subject: [PATCH 34/34] fixed sorting issue --- .../search-filter/search-filter.service.ts | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 890b32b2f0..44d9c7e709 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -57,14 +57,13 @@ export class SearchFilterService { }); } - getCurrentSort(): Observable { + getCurrentSort(defaultSort: SortOptions): Observable { const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); const sortField$ = this.routeService.getQueryParameterValue('sortField'); return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => { - if (isNotEmpty(sortField)) { - const direction = SortDirection[sortDirection]; - return new SortOptions(sortField, direction ? direction : SortDirection.ASC) - } + const field = sortField || defaultSort.field; + const direction = SortDirection[sortDirection] || defaultSort.direction; + return new SortOptions(field, direction) } ); } @@ -80,28 +79,28 @@ export class SearchFilterService { getPaginatedSearchOptions(defaults: any = {}): Observable { return Observable.combineLatest( this.getCurrentPagination(defaults.pagination), - this.getCurrentSort(), + this.getCurrentSort(defaults.sort), this.getCurrentView(), this.getCurrentScope(), this.getCurrentQuery(), this.getCurrentFilters()).pipe( - distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), - map(([pagination, sort, view, scope, query, filters]) => { - return Object.assign(new PaginatedSearchOptions(), - defaults, - { - pagination: pagination, - sort: sort || defaults.sort, - view: view, - scope: scope || defaults.scope, - query: query, - filters: filters - }) - }) - ) + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(([pagination, sort, view, scope, query, filters]) => { + return Object.assign(new PaginatedSearchOptions(), + defaults, + { + pagination: pagination, + sort: sort, + view: view, + scope: scope || defaults.scope, + query: query, + filters: filters + }) + }) + ) } - getSearchOptions(defaults: any = {}): Observable { + getSearchOptions(defaults: any = {}): Observable { return Observable.combineLatest( this.getCurrentView(), this.getCurrentScope(),