From 9ebd25709d91ce5af8e239fe8d8a6ca3a8dcedc5 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 30 Jul 2018 16:08:53 +0200 Subject: [PATCH] fixed configuration --- .../search-boolean-filter.component.html | 11 +- .../search-boolean-filter.component.scss | 2 + .../search-boolean-filter.component.ts | 1 - .../search-facet-filter.component.spec.ts | 8 +- .../search-facet-filter.component.ts | 6 +- .../search-filter.service.spec.ts | 2 +- .../search-filter/search-filter.service.ts | 139 +------------- .../search-range-filter.component.spec.ts | 8 +- .../search-range-filter.component.ts | 4 +- .../search-filters.component.spec.ts | 5 +- .../search-filters.component.ts | 8 +- .../search-labels.component.spec.ts | 4 +- .../search-labels/search-labels.component.ts | 6 +- .../+search-page/search-page.component.html | 62 +++---- .../search-page.component.spec.ts | 6 +- src/app/+search-page/search-page.component.ts | 21 +-- src/app/+search-page/search-page.module.ts | 4 +- .../search-configuration.service.spec.ts | 130 +++++++++++++ .../search-configuration.service.ts | 174 ++++++++++++++++++ .../search-service/search.service.spec.ts | 6 +- .../search-service/search.service.ts | 28 ++- .../search-settings.component.spec.ts | 7 +- .../search-settings.component.ts | 18 +- src/app/core/data/pid.service.ts | 3 +- .../search-form/search-form.component.ts | 2 +- src/app/shared/utils/debounce.directive.ts | 1 - 26 files changed, 409 insertions(+), 257 deletions(-) create mode 100644 src/app/+search-page/search-service/search-configuration.service.spec.ts create mode 100644 src/app/+search-page/search-service/search-configuration.service.ts diff --git a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html index 74abbd8e93..32d9ea6e77 100644 --- a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html @@ -30,14 +30,5 @@ | translate}} - + diff --git a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss index 33e354f2d8..030184640e 100644 --- a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss +++ b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.scss @@ -21,3 +21,5 @@ font-weight: bold; font-style: normal; } + + diff --git a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.ts index 4849578b93..5deaa34d29 100644 --- a/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.ts @@ -6,7 +6,6 @@ import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; - @Component({ selector: 'ds-search-boolean-filter', styleUrls: ['./search-boolean-filter.component.scss'], 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 f3336f780c..bdad302737 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 @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service'; @@ -12,12 +12,12 @@ 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 { PageInfo } from '../../../../core/shared/page-info.model'; import { SearchFacetFilterComponent } from './search-facet-filter.component'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; describe('SearchFacetFilterComponent', () => { let comp: SearchFacetFilterComponent; @@ -66,6 +66,7 @@ describe('SearchFacetFilterComponent', () => { { provide: Router, useValue: new RouterStub() }, { provide: FILTER_CONFIG, useValue: new SearchFilterConfig() }, { provide: RemoteDataBuildService, useValue: {aggregate: () => Observable.of({})} }, + { provide: SearchConfigurationService, useValue: {getSearchOptions: () => Observable.of({})} }, { provide: SearchFilterService, useValue: { getSelectedValuesForFilter: () => Observable.of(selectedValues), @@ -75,8 +76,7 @@ describe('SearchFacetFilterComponent', () => { incrementPage: (filterName: string) => { }, resetPage: (filterName: string) => { - }, - getSearchOptions: () => Observable.of({}), + } /* tslint:enable:no-empty */ } } 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 f5996ab4f0..8f71704a3e 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 @@ -15,6 +15,7 @@ import { FacetValue } from '../../../search-service/facet-value.model'; import { SearchFilterConfig } from '../../../search-service/search-filter-config.model'; import { SearchService } from '../../../search-service/search.service'; import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service'; +import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; @Component({ selector: 'ds-search-facet-filter', @@ -68,6 +69,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { constructor(protected searchService: SearchService, protected filterService: SearchFilterService, + protected searchConfigService: SearchConfigurationService, protected rdbs: RemoteDataBuildService, protected router: Router, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) { @@ -80,7 +82,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined)); this.currentPage = this.getCurrentPage().distinctUntilChanged(); this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig); - const searchOptions = this.filterService.getSearchOptions().distinctUntilChanged(); + const searchOptions = this.searchConfigService.getSearchOptions().distinctUntilChanged(); this.subs.push(searchOptions.subscribe((options) => this.updateFilterValueList())); const facetValues = Observable.combineLatest(searchOptions, this.currentPage, (options, page) => { @@ -240,7 +242,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { */ findSuggestions(data): void { if (isNotEmpty(data)) { - this.filterService.getSearchOptions().first().subscribe( + this.searchConfigService.getSearchOptions().first().subscribe( (options) => { this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase()) .first() 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 b2d6847aac..6d250f6869 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 @@ -55,7 +55,7 @@ describe('SearchFilterService', () => { }; beforeEach(() => { - service = new SearchFilterService(store, routeServiceStub, activatedRoute); + service = new SearchFilterService(store, routeServiceStub); }); describe('when the initialCollapse method is triggered', () => { 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 2e61b308a9..3b7c7b8e86 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 @@ -33,8 +33,8 @@ export const FILTER_CONFIG: InjectionToken = new InjectionTo export class SearchFilterService { constructor(private store: Store, - private routeService: RouteService, - private route: ActivatedRoute) { + private routeService: RouteService + ) { } /** @@ -56,141 +56,6 @@ export class SearchFilterService { return this.routeService.hasQueryParam(paramName); } - /** - * @returns {Observable} Emits the current scope's identifier - */ - getCurrentScope() { - return this.routeService.getQueryParameterValue('scope'); - } - - /** - * @returns {Observable} Emits the current query string - */ - getCurrentQuery() { - return this.routeService.getQueryParameterValue('query'); - } - - /** - * @returns {Observable} Emits the current pagination settings - */ - 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 - }); - }); - } - - /** - * @returns {Observable} Emits the current sorting settings - */ - getCurrentSort(defaultSort: SortOptions): Observable { - const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); - const sortField$ = this.routeService.getQueryParameterValue('sortField'); - return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => { - // Dirty fix because sometimes the observable value is null somehow - sortField = this.route.snapshot.queryParamMap.get('sortField'); - - const field = sortField || defaultSort.field; - const direction = SortDirection[sortDirection] || defaultSort.direction; - return new SortOptions(field, direction) - } - ); - } - - /** - * @returns {Observable} Emits the current active filters with their values as they are sent to the backend - */ - getCurrentFilters(): Observable { - return this.routeService.getQueryParamsWithPrefix('f.').map((filterParams) => { - if (isNotEmpty(filterParams)) { - const params = {}; - Object.keys(filterParams).forEach((key) => { - if (key.endsWith('.min') || key.endsWith('.max')) { - const realKey = key.slice(0, -4); - if (isEmpty(params[realKey])) { - const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*'; - const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*'; - params[realKey] = ['[' + min + ' TO ' + max + ']']; - } - } else { - params[key] = filterParams[key]; - } - }); - return params; - } - return filterParams; - }); - } - - /** - * @returns {Observable} Emits the current active filters with their values as they are displayed in the frontend URL - */ - getCurrentFrontendFilters(): Observable { - return this.routeService.getQueryParamsWithPrefix('f.'); - } - - /** - * @returns {Observable} Emits the current UI list view - */ - getCurrentView() { - return this.routeService.getQueryParameterValue('view'); - } - - /** - * @param defaults The default values for the search options, that will be used if nothing is explicitly set - * @returns {Observable} Emits the current paginated search options - */ - getPaginatedSearchOptions(defaults: any = {}): Observable { - return Observable.combineLatest( - this.getCurrentPagination(defaults.pagination), - 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, - view: view, - scope: scope || defaults.scope, - query: query, - filters: filters - }) - }) - ) - } - - /** - * @param defaults The default values for the search options, that will be used if nothing is explicitly set - * @returns {Observable} Emits the current search options - */ - 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 || defaults.scope, - query: query, - filters: filters - }) - } - ) - } - /** * Requests the active filter values set for a given filter * @param {SearchFilterConfig} filterConfig The configuration for which the filters are active diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 731d8b8ca8..739858c2f5 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -18,6 +18,7 @@ import { PageInfo } from '../../../../core/shared/page-info.model'; import { SearchRangeFilterComponent } from './search-range-filter.component'; import { RouteService } from '../../../../shared/services/route.service'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; describe('SearchRangeFilterComponent', () => { let comp: SearchRangeFilterComponent; @@ -72,6 +73,9 @@ describe('SearchRangeFilterComponent', () => { { provide: FILTER_CONFIG, useValue: mockFilterConfig }, { provide: RemoteDataBuildService, useValue: {aggregate: () => Observable.of({})} }, { provide: RouteService, useValue: {getQueryParameterValue: () => Observable.of({})} }, + { provide: SearchConfigurationService, useValue: { + getSearchOptions: () => Observable.of({}) } + }, { provide: SearchFilterService, useValue: { getSelectedValuesForFilter: () => selectedValues, @@ -81,8 +85,7 @@ describe('SearchRangeFilterComponent', () => { incrementPage: (filterName: string) => { }, resetPage: (filterName: string) => { - }, - getSearchOptions: () => Observable.of({}), + } /* tslint:enable:no-empty */ } } @@ -113,7 +116,6 @@ describe('SearchRangeFilterComponent', () => { }); }); - describe('when the onSubmit method is called with data', () => { const searchUrl = '/search/path'; // const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' }; diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index b50c1aef86..e63a34600f 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -16,6 +16,7 @@ import { Observable } from 'rxjs/Observable'; import { RouteService } from '../../../../shared/services/route.service'; import { hasValue } from '../../../../shared/empty.util'; import { Subscription } from 'rxjs/Subscription'; +import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; /** * This component renders a simple item page. @@ -61,12 +62,13 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple constructor(protected searchService: SearchService, protected filterService: SearchFilterService, + protected searchConfigService: SearchConfigurationService, protected router: Router, protected rdbs: RemoteDataBuildService, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @Inject(PLATFORM_ID) private platformId: any, private route: RouteService) { - super(searchService, filterService, rdbs, router, filterConfig); + super(searchService, filterService, searchConfigService, rdbs, router, filterConfig); } 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 6557c5e55f..64e3a1d2c7 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 @@ -8,6 +8,7 @@ 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'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; describe('SearchFiltersComponent', () => { let comp: SearchFiltersComponent; @@ -23,7 +24,7 @@ describe('SearchFiltersComponent', () => { } /* tslint:enable:no-empty */ }; - const searchFilterServiceStub = jasmine.createSpyObj('SearchFilterService', { + const searchConfigServiceStub = jasmine.createSpyObj('SearchConfigurationService', { getCurrentFrontendFilters: Observable.of({}) }); @@ -33,7 +34,7 @@ describe('SearchFiltersComponent', () => { declarations: [SearchFiltersComponent], providers: [ { provide: SearchService, useValue: searchServiceStub }, - { provide: SearchFilterService, useValue: searchFilterServiceStub }, + { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] 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 e79582d88b..9c2a0c94be 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -3,7 +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'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; @Component({ selector: 'ds-search-filters', @@ -29,11 +29,11 @@ export class SearchFiltersComponent { /** * Initialize instance variables * @param {SearchService} searchService - * @param {SearchFilterService} filterService + * @param {SearchConfigurationService} searchConfigService */ - constructor(private searchService: SearchService, private filterService: SearchFilterService) { + constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService) { this.filters = searchService.getConfig(); - this.clearParams = filterService.getCurrentFrontendFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;}); + this.clearParams = searchConfigService.getCurrentFrontendFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;}); } /** diff --git a/src/app/+search-page/search-labels/search-labels.component.spec.ts b/src/app/+search-page/search-labels/search-labels.component.spec.ts index 3df5b7788c..bf512ed5db 100644 --- a/src/app/+search-page/search-labels/search-labels.component.spec.ts +++ b/src/app/+search-page/search-labels/search-labels.component.spec.ts @@ -9,7 +9,7 @@ import { SearchServiceStub } from '../../shared/testing/search-service-stub'; import { Observable } from 'rxjs/Observable'; import { Params } from '@angular/router'; import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe'; -import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; describe('SearchLabelsComponent', () => { let comp: SearchLabelsComponent; @@ -35,7 +35,7 @@ describe('SearchLabelsComponent', () => { declarations: [SearchLabelsComponent, ObjectKeysPipe], providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, - { provide: SearchFilterService, useValue: {getCurrentFrontendFilters : () => Observable.of({})} } + { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => Observable.of({})} } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(SearchLabelsComponent, { diff --git a/src/app/+search-page/search-labels/search-labels.component.ts b/src/app/+search-page/search-labels/search-labels.component.ts index 7d8b8fbd7a..f5215d7997 100644 --- a/src/app/+search-page/search-labels/search-labels.component.ts +++ b/src/app/+search-page/search-labels/search-labels.component.ts @@ -3,8 +3,8 @@ import { SearchService } from '../search-service/search.service'; import { Observable } from 'rxjs/Observable'; import { Params } from '@angular/router'; import { map } from 'rxjs/operators'; -import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; @Component({ selector: 'ds-search-labels', @@ -23,8 +23,8 @@ export class SearchLabelsComponent { /** * Initialize the instance variable */ - constructor(private searchService: SearchService, private filterService: SearchFilterService) { - this.appliedFilters = this.filterService.getCurrentFrontendFilters(); + constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService) { + this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters(); } /** diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 0112cb14c4..653f5e8cd4 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -1,40 +1,40 @@
-
+
- - - -
-
- - -
-
- - + + + +
+
+ + +
+
+ + +
+ +
+
- -
-
-
diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index a6b210b393..a381c9c5d2 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -19,6 +19,7 @@ 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'; +import { SearchConfigurationService } from './search-service/search-configuration.service'; describe('SearchPageComponent', () => { let comp: SearchPageComponent; @@ -89,7 +90,10 @@ describe('SearchPageComponent', () => { }, { provide: SearchFilterService, - useValue: jasmine.createSpyObj('SearchFilterService', { + useValue: {} + },{ + provide: SearchConfigurationService, + useValue: jasmine.createSpyObj('SearchConfigurationService', { getPaginatedSearchOptions: hot('a', { a: paginatedSearchOptions }), diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 267244d4d3..ad2a5a0fcd 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -15,6 +15,7 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { Subscription } from 'rxjs/Subscription'; import { hasValue } from '../shared/empty.util'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { SearchConfigurationService } from './search-service/search-configuration.service'; /** * This component renders a simple item page. @@ -55,19 +56,6 @@ export class SearchPageComponent implements OnInit { */ isXsOrSm$: Observable; - /** - * Default values for the Search Options - */ - defaults = { - pagination: { - id: 'search-results-pagination', - pageSize: 10 - }, - sort: new SortOptions('score', SortDirection.DESC), - query: '', - scope: '' - }; - /** * Subscription to unsubscribe from */ @@ -76,7 +64,8 @@ export class SearchPageComponent implements OnInit { constructor(private service: SearchService, private sidebarService: SearchSidebarService, private windowService: HostWindowService, - private filterService: SearchFilterService) { + private filterService: SearchFilterService, + private searchConfigService: SearchConfigurationService) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -88,11 +77,11 @@ export class SearchPageComponent implements OnInit { * If something changes, update the list of scopes for the dropdown */ ngOnInit(): void { - this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults); + this.searchOptions$ = this.searchConfigService.getPaginatedSearchOptions(); this.sub = this.searchOptions$.subscribe((searchOptions) => this.service.search(searchOptions).filter((rd) => !rd.isLoading).first().subscribe((results) => this.resultsRD$.next(results))); - this.scopeListRD$ = this.filterService.getCurrentScope().pipe( + this.scopeListRD$ = this.searchConfigService.getCurrentScope().pipe( flatMap((scopeId) => this.service.getScopes(scopeId)) ); } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index cfe7dc128a..0c8a4ee306 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -27,6 +27,7 @@ import { SearchTextFilterComponent } from './search-filters/search-filter/search import { SearchFacetFilterWrapperComponent } from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component'; import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component'; import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component'; +import { SearchConfigurationService } from './search-service/search-configuration.service'; const effects = [ SearchSidebarEffects @@ -66,7 +67,8 @@ const effects = [ providers: [ SearchService, SearchSidebarService, - SearchFilterService + SearchFilterService, + SearchConfigurationService ], entryComponents: [ ItemSearchResultListElementComponent, diff --git a/src/app/+search-page/search-service/search-configuration.service.spec.ts b/src/app/+search-page/search-service/search-configuration.service.spec.ts new file mode 100644 index 0000000000..a06edc482a --- /dev/null +++ b/src/app/+search-page/search-service/search-configuration.service.spec.ts @@ -0,0 +1,130 @@ +import { SearchConfigurationService } from './search-configuration.service'; +import { Observable } from 'rxjs/Observable'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { RemoteData } from '../../core/data/remote-data'; +import { fakeAsync, tick } from '@angular/core/testing'; + +describe('SearchConfigurationService', () => { + let service: SearchConfigurationService; + const value1 = 'random value'; + const value2 = 'another value'; + const prefixFilter = { + 'f.author': ['another value'], + 'f.date.min': ['2013'], + 'f.date.max': ['2018'] + }; + const defaults = Observable.of(new RemoteData(false, false, true, null, {})) + const backendFilters = { 'f.author': ['another value'], 'f.date': ['[2013 TO 2018]'] }; + + const spy = jasmine.createSpyObj('SearchConfigurationService', { + getQueryParameterValue: Observable.of([value1, value2]) + , + getQueryParamsWithPrefix: Observable.of(prefixFilter) + }); + + const activatedRoute: any = new ActivatedRouteStub(); + + beforeEach(() => { + service = new SearchConfigurationService(spy, activatedRoute); + }); + + describe('when getCurrentScope is called', () => { + beforeEach(() => { + service.getCurrentScope(); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'scope\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('scope'); + }); + }); + + describe('when getCurrentQuery is called', () => { + beforeEach(() => { + service.getCurrentQuery(); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'query\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('query'); + }); + }); + + describe('when getCurrentFrontendFilters is called', () => { + beforeEach(() => { + service.getCurrentFrontendFilters(); + }); + it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => { + expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.'); + }); + }); + + describe('when getCurrentFilters is called', () => { + let parsedValues$; + beforeEach(() => { + parsedValues$ = service.getCurrentFilters(); + }); + it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => { + expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.'); + parsedValues$.subscribe((values) => { + expect(values).toEqual(backendFilters); + }); + }); + }); + + describe('when getCurrentSort is called', () => { + beforeEach(() => { + service.getCurrentSort({} as any); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection'); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField'); + }); + }); + describe('when getCurrentPagination is called', () => { + beforeEach(() => { + service.getCurrentPagination({}); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page'); + }); + it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => { + expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize'); + }); + }); + fdescribe('when getPaginatedSearchOptions or getSearchOptions is called', () => { + beforeEach(() => { + spyOn(service, 'getCurrentPagination'); + spyOn(service, 'getCurrentSort'); + spyOn(service, 'getCurrentScope'); + spyOn(service, 'getCurrentQuery'); + spyOn(service, 'getCurrentFilters'); + }); + describe('when getPaginatedSearchOptions is called', () => { + beforeEach(() => { + service.getPaginatedSearchOptions(defaults); + }); + it('should call all getters it needs', fakeAsync(() => { + defaults.subscribe(() => { + tick(); + expect(service.getCurrentPagination).toHaveBeenCalled(); + expect(service.getCurrentSort).toHaveBeenCalled(); + expect(service.getCurrentScope).toHaveBeenCalled(); + expect(service.getCurrentQuery).toHaveBeenCalled(); + expect(service.getCurrentFilters).toHaveBeenCalled(); + } + ) + })); + }); + describe('when getSearchOptions is called', () => { + beforeEach(() => { + service.getSearchOptions(); + }); + it('should call all getters it needs', () => { + expect(service.getCurrentPagination).not.toHaveBeenCalled(); + expect(service.getCurrentSort).not.toHaveBeenCalled(); + expect(service.getCurrentScope).toHaveBeenCalled(); + expect(service.getCurrentQuery).toHaveBeenCalled(); + expect(service.getCurrentFilters).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts new file mode 100644 index 0000000000..b7cd43217c --- /dev/null +++ b/src/app/+search-page/search-service/search-configuration.service.ts @@ -0,0 +1,174 @@ +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 { distinctUntilChanged, map } from 'rxjs/operators'; +import { Observable } from 'rxjs/Observable'; +import { ActivatedRoute, Params } from '@angular/router'; +import { PaginatedSearchOptions } from '../paginated-search-options.model'; +import { Injectable } from '@angular/core'; +import { RouteService } from '../../shared/services/route.service'; +import { hasNoValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { RemoteData } from '../../core/data/remote-data'; + +/** + * Service that performs all actions that have to do with the current search configuration + */ +@Injectable() +export class SearchConfigurationService { + private defaultPagination = { id: 'search-page-configuration', pageSize: 10 }; + + private defaultSort = new SortOptions('score', SortDirection.DESC); + + private defaultScope = ''; + + private _defaults; + + constructor(private routeService: RouteService, + private route: ActivatedRoute) { + + } + + /** + * @returns {Observable} Emits the current scope's identifier + */ + getCurrentScope() { + return this.routeService.getQueryParameterValue('scope'); + } + + /** + * @returns {Observable} Emits the current query string + */ + getCurrentQuery() { + return this.routeService.getQueryParameterValue('query'); + } + + /** + * @returns {Observable} Emits the current pagination settings + */ + 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 + }); + }); + } + + /** + * @returns {Observable} Emits the current sorting settings + */ + getCurrentSort(defaultSort: SortOptions): Observable { + const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); + const sortField$ = this.routeService.getQueryParameterValue('sortField'); + return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => { + // Dirty fix because sometimes the observable value is null somehow + sortField = this.route.snapshot.queryParamMap.get('sortField'); + + const field = sortField || defaultSort.field; + const direction = SortDirection[sortDirection] || defaultSort.direction; + return new SortOptions(field, direction) + } + ); + } + + /** + * @returns {Observable} Emits the current active filters with their values as they are sent to the backend + */ + getCurrentFilters(): Observable { + return this.routeService.getQueryParamsWithPrefix('f.').map((filterParams) => { + if (isNotEmpty(filterParams)) { + const params = {}; + Object.keys(filterParams).forEach((key) => { + if (key.endsWith('.min') || key.endsWith('.max')) { + const realKey = key.slice(0, -4); + if (isEmpty(params[realKey])) { + const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*'; + const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*'; + params[realKey] = ['[' + min + ' TO ' + max + ']']; + } + } else { + params[key] = filterParams[key]; + } + }); + return params; + } + return filterParams; + }); + } + + /** + * @returns {Observable} Emits the current active filters with their values as they are displayed in the frontend URL + */ + getCurrentFrontendFilters(): Observable { + return this.routeService.getQueryParamsWithPrefix('f.'); + } + + /** + * @param defaults The default values for the search options, that will be used if nothing is explicitly set + * @returns {Observable} Emits the current paginated search options + */ + getPaginatedSearchOptions(defaults: Observable> = this.defaults): Observable { + return defaults.flatMap((defaultConfig) => { + const defaultValues = defaultConfig.payload; + return Observable.combineLatest( + this.getCurrentPagination(defaultValues.pagination), + this.getCurrentSort(defaultValues.sort), + this.getCurrentScope(), + this.getCurrentQuery(), + this.getCurrentFilters()).pipe( + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(([pagination, sort, scope, query, filters]) => { + return Object.assign(new PaginatedSearchOptions(), + defaults, + { + pagination: pagination, + sort: sort, + scope: scope || defaultValues.scope, + query: query, + filters: filters + }) + }) + ) + }); + } + + /** + * @param defaults The default values for the search options, that will be used if nothing is explicitly set + * @returns {Observable} Emits the current search options + */ + getSearchOptions(defaults: Observable> = this.defaults): Observable { + return defaults.flatMap((defaultConfig) => { + const defaultValues = defaultConfig.payload; + return Observable.combineLatest( + this.getCurrentScope(), + this.getCurrentQuery(), + this.getCurrentFilters(), + (scope, query, filters) => { + return Object.assign(new SearchOptions(), + { + scope: scope || defaultValues.scope, + query: query, + filters: filters + }) + } + ) + }); + } + + /** + * Default values for the Search Options + */ + get defaults() { + if (hasNoValue(this._defaults)) { + const options = { + pagination: this.defaultPagination, + sort: this.defaultSort, + scope: this.defaultScope + }; + this._defaults = Observable.of(new RemoteData(false, false, true, null, options)); + } + return this._defaults; + } +} 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 83ca7ae3d0..4cd911e2e6 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -25,9 +25,9 @@ import { } from '../../core/cache/response-cache.models'; import { SearchQueryResponse } from './search-query-response.model'; import { SearchFilterConfig } from './search-filter-config.model'; -import { CollectionDataService } from '../../core/data/collection-data.service'; import { CommunityDataService } from '../../core/data/community-data.service'; import { ViewMode } from '../../core/shared/view-mode.model'; +import { PIDService } from '../../core/data/pid.service'; @Component({ template: '' }) class DummyComponent { @@ -57,7 +57,7 @@ describe('SearchService', () => { { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, { provide: CommunityDataService, useValue: {}}, - { provide: CollectionDataService, useValue: {}}, + { provide: PIDService, useValue: {}}, SearchService ], }); @@ -114,7 +114,7 @@ describe('SearchService', () => { { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: HALEndpointService, useValue: halService }, { provide: CommunityDataService, useValue: {}}, - { provide: CollectionDataService, useValue: {}}, + { provide: PIDService, 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 457f993b77..3ca200330d 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -26,7 +26,7 @@ import { GenericConstructor } from '../../core/shared/generic-constructor'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { configureRequest } from '../../core/shared/operators'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedSearchResult } from '../normalized-search-result.model'; import { SearchOptions } from '../search-options.model'; import { SearchResult } from '../search-result.model'; @@ -42,10 +42,9 @@ import { FacetConfigResponseParsingService } from '../../core/data/facet-config- import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { CollectionDataService } from '../../core/data/collection-data.service'; -import { Collection } from '../../core/shared/collection.model'; import { ViewMode } from '../../core/shared/view-mode.model'; - +import { PIDService } from '../../core/data/pid.service'; +import { ResourceType } from '../../core/shared/resource-type'; /** * Service that performs all general actions that have to do with the search page @@ -74,7 +73,8 @@ export class SearchService implements OnDestroy { private rdb: RemoteDataBuildService, private halService: HALEndpointService, private communityService: CommunityDataService, - private collectionService: CollectionDataService) { + private pidService: PIDService + ) { } /** @@ -258,7 +258,7 @@ export class SearchService implements OnDestroy { */ getScopes(scopeId?: string): Observable { - if (hasNoValue(scopeId)) { + if (isEmpty(scopeId)) { const top: Observable = this.communityService.findTop({ elementsPerPage: 9999 }).pipe( map( (communities: RemoteData>) => communities.payload.page @@ -267,24 +267,22 @@ export class SearchService implements OnDestroy { return top; } - const communityScope: Observable> = this.communityService.findById(scopeId).filter((communityRD: RemoteData) => !communityRD.isLoading); - const scopeObject: Observable = communityScope.pipe( - flatMap((communityRD: RemoteData) => { - if (hasValue(communityRD.payload)) { - const community: Community = communityRD.payload; - // const subcommunities$ = community.subcommunities.filter((subcommunitiesRD: RemoteData>) => !subcommunitiesRD.isLoading).first(); - // const collections$ = community.subcommunities.filter((subcommunitiesRD: RemoteData>) => !subcommunitiesRD.isLoading).first(); + const scopeObject: Observable> = this.pidService.findById(scopeId).filter((dsoRD: RemoteData) => !dsoRD.isLoading); + const scopeList: Observable = scopeObject.pipe( + flatMap((dsoRD: RemoteData) => { + if (dsoRD.payload.type === ResourceType.Community) { + const community: Community = dsoRD.payload as Community; return Observable.combineLatest(community.subcommunities, community.collections, (subCommunities, collections) => { /*if this is a community, we also need to show the direct children*/ return [community, ...subCommunities.payload.page, ...collections.payload.page] }) } else { - return this.collectionService.findById(scopeId).pipe(map((collectionRD: RemoteData) => [collectionRD.payload])); + return Observable.of([dsoRD.payload]); } } )); - return scopeObject; + return scopeList; } 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 fb90d4b703..898af1c2d7 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 @@ -14,6 +14,7 @@ import { By } from '@angular/platform-browser'; import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { hot } from 'jasmine-marbles'; import { VarDirective } from '../../shared/utils/var.directive'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; describe('SearchSettingsComponent', () => { @@ -68,7 +69,11 @@ describe('SearchSettingsComponent', () => { }, { provide: SearchFilterService, - useValue: jasmine.createSpyObj('SearchFilterService', { + useValue: {} + }, + { + provide: SearchConfigurationService, + useValue: jasmine.createSpyObj('SearchConfigurationService', { getPaginatedSearchOptions: hot('a', { a: paginatedSearchOptions }), 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 c1778b37f4..f175c1a871 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { Observable } from 'rxjs/Observable'; +import { SearchConfigurationService } from '../search-service/search-configuration.service'; @Component({ selector: 'ds-search-settings', @@ -27,30 +28,17 @@ export class SearchSettingsComponent implements OnInit { */ searchOptionPossibilities = [new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC)]; - /** - * Default values for the Search Options - */ - defaults = { - pagination: { - id: 'search-results-pagination', - pageSize: 10 - }, - sort: new SortOptions('score', SortDirection.DESC), - query: '', - scope: '' - }; - constructor(private service: SearchService, private route: ActivatedRoute, private router: Router, - private filterService: SearchFilterService) { + private searchConfigurationService: SearchConfigurationService) { } /** * Initialize paginated search options */ ngOnInit(): void { - this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults); + this.searchOptions$ = this.searchConfigurationService.getPaginatedSearchOptions(); } /** diff --git a/src/app/core/data/pid.service.ts b/src/app/core/data/pid.service.ts index 07ccf66d9a..8c745a232d 100644 --- a/src/app/core/data/pid.service.ts +++ b/src/app/core/data/pid.service.ts @@ -23,7 +23,6 @@ class DataServiceImpl extends DataService protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - private bs: BrowseService, protected halService: HALEndpointService) { super(); } @@ -46,7 +45,7 @@ export class PIDService { protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, null, halService); + this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService); } findById(id: string): Observable> { diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index c9942bbf7d..a556e4c036 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -1,5 +1,4 @@ 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, hasNoValue } from '../empty.util'; @@ -31,6 +30,7 @@ export class SearchFormComponent { */ @Input() scope = ''; + @Input() currentUrl: string; /** diff --git a/src/app/shared/utils/debounce.directive.ts b/src/app/shared/utils/debounce.directive.ts index f3d3e4aa25..6a39a598c0 100644 --- a/src/app/shared/utils/debounce.directive.ts +++ b/src/app/shared/utils/debounce.directive.ts @@ -31,7 +31,6 @@ export class DebounceDirective implements OnInit, OnDestroy { */ private isFirstChange = true; - /** * Subject to unsubscribe from */