fixed configuration

This commit is contained in:
lotte
2018-07-30 16:08:53 +02:00
parent c05a0e6d6a
commit 9ebd25709d
26 changed files with 409 additions and 257 deletions

View File

@@ -30,14 +30,5 @@
| translate}}</a>
</div>
</div>
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
[action]="getCurrentUrl()"
[name]="filterConfig.paramName"
[(ngModel)]="filter"
(submitSuggestion)="onSubmit($event)"
(clickSuggestion)="onClick($event)"
(findSuggestions)="findSuggestions($event)"
ngDefaultControl
></ds-input-suggestions>
</div>

View File

@@ -21,3 +21,5 @@
font-weight: bold;
font-style: normal;
}

View File

@@ -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'],

View File

@@ -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 */
}
}

View File

@@ -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()

View File

@@ -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', () => {

View File

@@ -33,8 +33,8 @@ export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionTo
export class SearchFilterService {
constructor(private store: Store<SearchFiltersState>,
private routeService: RouteService,
private route: ActivatedRoute) {
private routeService: RouteService
) {
}
/**
@@ -56,141 +56,6 @@ export class SearchFilterService {
return this.routeService.hasQueryParam(paramName);
}
/**
* @returns {Observable<string>} Emits the current scope's identifier
*/
getCurrentScope() {
return this.routeService.getQueryParameterValue('scope');
}
/**
* @returns {Observable<string>} Emits the current query string
*/
getCurrentQuery() {
return this.routeService.getQueryParameterValue('query');
}
/**
* @returns {Observable<string>} Emits the current pagination settings
*/
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
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<string>} Emits the current sorting settings
*/
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
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<Params>} Emits the current active filters with their values as they are sent to the backend
*/
getCurrentFilters(): Observable<Params> {
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<Params>} Emits the current active filters with their values as they are displayed in the frontend URL
*/
getCurrentFrontendFilters(): Observable<Params> {
return this.routeService.getQueryParamsWithPrefix('f.');
}
/**
* @returns {Observable<string>} 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<PaginatedSearchOptions>} Emits the current paginated search options
*/
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
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<PaginatedSearchOptions>} Emits the current search options
*/
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
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

View File

@@ -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' };

View File

@@ -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);
}

View File

@@ -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]

View File

@@ -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;});
}
/**

View File

@@ -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, {

View File

@@ -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();
}
/**

View File

@@ -1,40 +1,40 @@
<div class="container">
<div class="search-page row">
<div class="search-page row">
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
id="search-sidebar"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"></ds-search-sidebar>
<div class="col-12 col-md-9">
<ds-search-form id="search-form"
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="getSearchLink()"
[scopes]="(scopeListRD$ | async)">
</ds-search-form>
<ds-search-labels></ds-search-labels>
<div class="row">
<div id="search-body"
class="row-offcanvas row-offcanvas-left"
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
</ds-search-sidebar>
<div id="search-content" class="col-12">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
class="fa fa-sliders"></i> {{"search.sidebar.open"
| translate}}
</button>
<ds-search-form id="search-form"
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="getSearchLink()"
[scopes]="(scopeListRD$ | async)">
</ds-search-form>
<ds-search-labels></ds-search-labels>
<div class="row">
<div id="search-body"
class="row-offcanvas row-offcanvas-left"
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
</ds-search-sidebar>
<div id="search-content" class="col-12">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
class="fa fa-sliders"></i> {{"search.sidebar.open"
| translate}}
</button>
</div>
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"></ds-search-results>
</div>
</div>
</div>
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"></ds-search-results>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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
}),

View File

@@ -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<boolean>;
/**
* 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))
);
}

View File

@@ -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,

View File

@@ -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();
});
});
});
});

View File

@@ -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<string>} Emits the current scope's identifier
*/
getCurrentScope() {
return this.routeService.getQueryParameterValue('scope');
}
/**
* @returns {Observable<string>} Emits the current query string
*/
getCurrentQuery() {
return this.routeService.getQueryParameterValue('query');
}
/**
* @returns {Observable<string>} Emits the current pagination settings
*/
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
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<string>} Emits the current sorting settings
*/
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
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<Params>} Emits the current active filters with their values as they are sent to the backend
*/
getCurrentFilters(): Observable<Params> {
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<Params>} Emits the current active filters with their values as they are displayed in the frontend URL
*/
getCurrentFrontendFilters(): Observable<Params> {
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<PaginatedSearchOptions>} Emits the current paginated search options
*/
getPaginatedSearchOptions(defaults: Observable<RemoteData<any>> = this.defaults): Observable<PaginatedSearchOptions> {
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<PaginatedSearchOptions>} Emits the current search options
*/
getSearchOptions(defaults: Observable<RemoteData<any>> = this.defaults): Observable<SearchOptions> {
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;
}
}

View File

@@ -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
],
});

View File

@@ -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<DSpaceObject[]> {
if (hasNoValue(scopeId)) {
if (isEmpty(scopeId)) {
const top: Observable<Community[]> = this.communityService.findTop({ elementsPerPage: 9999 }).pipe(
map(
(communities: RemoteData<PaginatedList<Community>>) => communities.payload.page
@@ -267,24 +267,22 @@ export class SearchService implements OnDestroy {
return top;
}
const communityScope: Observable<RemoteData<Community>> = this.communityService.findById(scopeId).filter((communityRD: RemoteData<Community>) => !communityRD.isLoading);
const scopeObject: Observable<DSpaceObject[]> = communityScope.pipe(
flatMap((communityRD: RemoteData<Community>) => {
if (hasValue(communityRD.payload)) {
const community: Community = communityRD.payload;
// const subcommunities$ = community.subcommunities.filter((subcommunitiesRD: RemoteData<PaginatedList<Community>>) => !subcommunitiesRD.isLoading).first();
// const collections$ = community.subcommunities.filter((subcommunitiesRD: RemoteData<PaginatedList<Community>>) => !subcommunitiesRD.isLoading).first();
const scopeObject: Observable<RemoteData<DSpaceObject>> = this.pidService.findById(scopeId).filter((dsoRD: RemoteData<DSpaceObject>) => !dsoRD.isLoading);
const scopeList: Observable<DSpaceObject[]> = scopeObject.pipe(
flatMap((dsoRD: RemoteData<DSpaceObject>) => {
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<Collection>) => [collectionRD.payload]));
return Observable.of([dsoRD.payload]);
}
}
));
return scopeObject;
return scopeList;
}

View File

@@ -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
}),

View File

@@ -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();
}
/**

View File

@@ -23,7 +23,6 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
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<RemoteData<DSpaceObject>> {

View File

@@ -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;
/**

View File

@@ -31,7 +31,6 @@ export class DebounceDirective implements OnInit, OnDestroy {
*/
private isFirstChange = true;
/**
* Subject to unsubscribe from
*/