mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
111731: Made the code completely dependent on the SearchService#appliedFilters$ instead of relying on the route query parameters
This commit is contained in:
@@ -153,17 +153,6 @@ describe('SearchFilterService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the getSelectedValuesForFilter method is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(routeServiceStub, 'getQueryParameterValues');
|
|
||||||
service.getSelectedValuesForFilter(mockFilterConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getQueryParameterValues on the route service with the same parameters', () => {
|
|
||||||
expect(routeServiceStub.getQueryParameterValues).toHaveBeenCalledWith(mockFilterConfig.paramName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the getCurrentScope method is called', () => {
|
describe('when the getCurrentScope method is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(routeServiceStub, 'getQueryParameterValue');
|
spyOn(routeServiceStub, 'getQueryParameterValue');
|
||||||
|
@@ -136,27 +136,6 @@ export class SearchFilterService {
|
|||||||
return this.routeService.getQueryParameterValue('view');
|
return this.routeService.getQueryParameterValue('view');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the active filter values set for a given filter
|
|
||||||
* @param {SearchFilterConfig} filterConfig The configuration for which the filters are active
|
|
||||||
* @returns {Observable<string[]>} Emits the active filters for the given filter configuration
|
|
||||||
*/
|
|
||||||
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
|
||||||
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
|
|
||||||
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
|
|
||||||
map((params: Params) => [].concat(...Object.values(params))),
|
|
||||||
);
|
|
||||||
return observableCombineLatest(values$, prefixValues$).pipe(
|
|
||||||
map(([values, prefixValues]) => {
|
|
||||||
if (isNotEmpty(values)) {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
return prefixValues;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the found facet value suggestions for a given query
|
* Updates the found facet value suggestions for a given query
|
||||||
* Transforms the found values into display values
|
* Transforms the found values into display values
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { map, switchMap, take, tap } from 'rxjs/operators';
|
import { map, switchMap, take, tap, distinctUntilChanged } from 'rxjs/operators';
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { ResponseParsingService } from '../../data/parsing.service';
|
import { ResponseParsingService } from '../../data/parsing.service';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
@@ -61,7 +61,7 @@ class SearchDataService extends BaseDataService<any> {
|
|||||||
* Service that performs all general actions that have to do with the search page
|
* Service that performs all general actions that have to do with the search page
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService implements OnDestroy {
|
export class SearchService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint link path for retrieving general search results
|
* Endpoint link path for retrieving general search results
|
||||||
@@ -78,11 +78,6 @@ export class SearchService implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private request: GenericConstructor<RestRequest> = GetRequest;
|
private request: GenericConstructor<RestRequest> = GetRequest;
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to unsubscribe from
|
|
||||||
*/
|
|
||||||
private sub;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of SearchDataService to forward data service methods to
|
* Instance of SearchDataService to forward data service methods to
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +98,18 @@ export class SearchService implements OnDestroy {
|
|||||||
this.searchDataService = new SearchDataService();
|
this.searchDataService = new SearchDataService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently {@link AppliedFilter}s for the given filter.
|
||||||
|
*
|
||||||
|
* @param filterName The name of the filter
|
||||||
|
*/
|
||||||
|
getSelectedValuesForFilter(filterName: string): Observable<AppliedFilter[]> {
|
||||||
|
return this.appliedFilters$.pipe(
|
||||||
|
map((appliedFilters: AppliedFilter[]) => appliedFilters.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === filterName)),
|
||||||
|
distinctUntilChanged((previous: AppliedFilter[], next: AppliedFilter[]) => JSON.stringify(previous) === JSON.stringify(next)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to set service options
|
* Method to set service options
|
||||||
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
|
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
|
||||||
@@ -176,26 +183,11 @@ export class SearchService implements OnDestroy {
|
|||||||
return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to retrieve request entries for search results from the server
|
|
||||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
|
||||||
* @returns {Observable<RemoteData<SearchObjects<T>>>} Emits a paginated list with all search results found
|
|
||||||
*/
|
|
||||||
searchEntries<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<SearchObjects<T>>> {
|
|
||||||
const href$ = this.getEndpoint(searchOptions);
|
|
||||||
|
|
||||||
const sqr$ = href$.pipe(
|
|
||||||
switchMap((href: string) => this.rdb.buildFromHref<SearchObjects<T>>(href))
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.directlyAttachIndexableObjects(sqr$);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to directly attach the indexableObjects to search results, instead of using RemoteData.
|
* Method to directly attach the indexableObjects to search results, instead of using RemoteData.
|
||||||
* For compatibility with the way the search was written originally
|
* For compatibility with the way the search was written originally
|
||||||
*
|
*
|
||||||
* @param sqr$: a SearchObjects RemotaData Observable without its
|
* @param sqr$ A {@link SearchObjects} {@link RemoteData} Observable without its
|
||||||
* indexableObjects attached
|
* indexableObjects attached
|
||||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
* no valid cached version. Defaults to true
|
* no valid cached version. Defaults to true
|
||||||
@@ -384,12 +376,4 @@ export class SearchService implements OnDestroy {
|
|||||||
return '/search';
|
return '/search';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from the subscription
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.sub !== undefined) {
|
|
||||||
this.sub.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { Router, Params } from '@angular/router';
|
import { Router, Params } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, filter, map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { hasNoValue, hasValue } from '../../../../empty.util';
|
import { hasNoValue, hasValue } from '../../../../empty.util';
|
||||||
@@ -17,7 +17,6 @@ import { InputSuggestion } from '../../../../input-suggestions/input-suggestions
|
|||||||
import { SearchOptions } from '../../../models/search-options.model';
|
import { SearchOptions } from '../../../models/search-options.model';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { currentPath } from '../../../../utils/route.utils';
|
import { currentPath } from '../../../../utils/route.utils';
|
||||||
import { stripOperatorFromFilterValue } from '../../../search.utils';
|
|
||||||
import { FacetValues } from '../../../models/facet-values.model';
|
import { FacetValues } from '../../../models/facet-values.model';
|
||||||
import { AppliedFilter } from '../../../models/applied-filter.model';
|
import { AppliedFilter } from '../../../models/applied-filter.model';
|
||||||
|
|
||||||
@@ -103,14 +102,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
this.searchOptions$.subscribe(() => this.updateFilterValueList()),
|
this.searchOptions$.subscribe(() => this.updateFilterValueList()),
|
||||||
this.refreshFilters.asObservable().pipe(
|
this.retrieveFilterValues().subscribe(),
|
||||||
filter((toRefresh: boolean) => toRefresh),
|
|
||||||
// NOTE This is a workaround, otherwise retrieving filter values returns tha old cached response
|
|
||||||
debounceTime((100)),
|
|
||||||
mergeMap(() => this.retrieveFilterValues(false))
|
|
||||||
).subscribe()
|
|
||||||
);
|
);
|
||||||
this.retrieveFilterValues().subscribe();
|
this.selectedAppliedFilters$ = this.searchService.getSelectedValuesForFilter(this.filterConfig.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,9 +211,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected retrieveFilterValues(useCachedVersionIfAvailable = true): Observable<FacetValues[]> {
|
/**
|
||||||
|
* Retrieves all the filter value suggestion pages that need to be displayed in the facet and combines it into one
|
||||||
|
* list.
|
||||||
|
*/
|
||||||
|
protected retrieveFilterValues(): Observable<FacetValues[]> {
|
||||||
return observableCombineLatest([this.searchOptions$, this.currentPage]).pipe(
|
return observableCombineLatest([this.searchOptions$, this.currentPage]).pipe(
|
||||||
switchMap(([options, page]: [SearchOptions, number]) => this.searchService.getFacetValuesFor(this.filterConfig, page, options, null, useCachedVersionIfAvailable).pipe(
|
switchMap(([options, page]: [SearchOptions, number]) => this.searchService.getFacetValuesFor(this.filterConfig, page, options).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
tap((facetValues: FacetValues) => {
|
tap((facetValues: FacetValues) => {
|
||||||
this.isLastPage$.next(hasNoValue(facetValues?.next));
|
this.isLastPage$.next(hasNoValue(facetValues?.next));
|
||||||
@@ -242,27 +240,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
return filterValues;
|
return filterValues;
|
||||||
}),
|
}),
|
||||||
tap((allFacetValues: FacetValues[]) => {
|
tap((allFacetValues: FacetValues[]) => {
|
||||||
this.setAppliedFilter(allFacetValues);
|
|
||||||
this.animationState = 'ready';
|
this.animationState = 'ready';
|
||||||
this.facetValues$.next(allFacetValues);
|
this.facetValues$.next(allFacetValues);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppliedFilter(allFacetValues: FacetValues[]): void {
|
|
||||||
const allAppliedFilters: AppliedFilter[] = [].concat(...allFacetValues.map((facetValues: FacetValues) => facetValues.appliedFilters))
|
|
||||||
.filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter));
|
|
||||||
|
|
||||||
this.selectedAppliedFilters$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
|
|
||||||
map((selectedValues: string[]) => {
|
|
||||||
const appliedFilters: AppliedFilter[] = selectedValues.map((value: string) => {
|
|
||||||
return allAppliedFilters.find((appliedFilter: AppliedFilter) => appliedFilter.value === stripOperatorFromFilterValue(value));
|
|
||||||
}).filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter));
|
|
||||||
return appliedFilters;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unnecessary rerendering
|
* Prevent unnecessary rerendering
|
||||||
*/
|
*/
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -14,51 +14,28 @@ import { SearchConfigurationServiceStub } from '../../../testing/search-configur
|
|||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SequenceService } from '../../../../core/shared/sequence.service';
|
import { SequenceService } from '../../../../core/shared/sequence.service';
|
||||||
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
||||||
|
import { SearchServiceStub } from '../../../testing/search-service.stub';
|
||||||
|
import { SearchFilterServiceStub } from '../../../testing/search-filter-service.stub';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
let fixture: ComponentFixture<SearchFilterComponent>;
|
let fixture: ComponentFixture<SearchFilterComponent>;
|
||||||
const filterName1 = 'test name';
|
const filterName1 = 'test name';
|
||||||
const filterName2 = 'test2';
|
|
||||||
const filterName3 = 'another name3';
|
|
||||||
const nonExistingFilter1 = 'non existing 1';
|
|
||||||
const nonExistingFilter2 = 'non existing 2';
|
|
||||||
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
name: filterName1,
|
name: filterName1,
|
||||||
filterType: FilterType.text,
|
filterType: FilterType.text,
|
||||||
hasFacets: false,
|
hasFacets: false,
|
||||||
isOpenByDefault: false
|
isOpenByDefault: false
|
||||||
});
|
});
|
||||||
const mockFilterService = {
|
let searchFilterService: SearchFilterServiceStub;
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
toggle: (filter) => {
|
|
||||||
},
|
|
||||||
collapse: (filter) => {
|
|
||||||
},
|
|
||||||
expand: (filter) => {
|
|
||||||
},
|
|
||||||
initializeFilter: (filter) => {
|
|
||||||
},
|
|
||||||
getSelectedValuesForFilter: (filter) => {
|
|
||||||
return observableOf([filterName1, filterName2, filterName3]);
|
|
||||||
},
|
|
||||||
isFilterActive: (filter) => {
|
|
||||||
return observableOf([filterName1, filterName2, filterName3].indexOf(filter) >= 0);
|
|
||||||
},
|
|
||||||
isCollapsed: (filter) => {
|
|
||||||
return observableOf(true);
|
|
||||||
}
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
};
|
|
||||||
let filterService;
|
|
||||||
let sequenceService;
|
let sequenceService;
|
||||||
const mockResults = observableOf(['test', 'data']);
|
const mockResults = observableOf(['test', 'data']);
|
||||||
const searchServiceStub = {
|
let searchService: SearchServiceStub;
|
||||||
getFacetValuesFor: (filter) => mockResults
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
searchFilterService = new SearchFilterServiceStub();
|
||||||
|
searchService = new SearchServiceStub();
|
||||||
sequenceService = jasmine.createSpyObj('sequenceService', { next: 17 });
|
sequenceService = jasmine.createSpyObj('sequenceService', { next: 17 });
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -68,26 +45,23 @@ describe('SearchFilterComponent', () => {
|
|||||||
BrowserOnlyMockPipe,
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchService },
|
||||||
{
|
{ provide: SearchFilterService, useValue: searchFilterService },
|
||||||
provide: SearchFilterService,
|
|
||||||
useValue: mockFilterService
|
|
||||||
},
|
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: SequenceService, useValue: sequenceService },
|
{ provide: SequenceService, useValue: sequenceService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).overrideComponent(SearchFilterComponent, {
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockResults);
|
||||||
fixture = TestBed.createComponent(SearchFilterComponent);
|
fixture = TestBed.createComponent(SearchFilterComponent);
|
||||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
comp.filter = mockFilterConfig;
|
comp.filter = mockFilterConfig;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
filterService = (comp as any).filterService;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate unique IDs', () => {
|
it('should generate unique IDs', () => {
|
||||||
@@ -98,54 +72,30 @@ describe('SearchFilterComponent', () => {
|
|||||||
|
|
||||||
describe('when the toggle method is triggered', () => {
|
describe('when the toggle method is triggered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(filterService, 'toggle');
|
spyOn(searchFilterService, 'toggle');
|
||||||
comp.toggle();
|
comp.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call toggle with the correct filter configuration name', () => {
|
it('should call toggle with the correct filter configuration name', () => {
|
||||||
expect(filterService.toggle).toHaveBeenCalledWith(mockFilterConfig.name);
|
expect(searchFilterService.toggle).toHaveBeenCalledWith(mockFilterConfig.name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the initializeFilter method is triggered', () => {
|
describe('when the initializeFilter method is triggered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(filterService, 'initializeFilter');
|
spyOn(searchFilterService, 'initializeFilter');
|
||||||
comp.initializeFilter();
|
comp.initializeFilter();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call initialCollapse with the correct filter configuration name', () => {
|
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||||
expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig);
|
expect(searchFilterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when getSelectedValues is called', () => {
|
|
||||||
let valuesObservable: Observable<string[]>;
|
|
||||||
beforeEach(() => {
|
|
||||||
valuesObservable = (comp as any).getSelectedValues();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an observable containing the existing filters', () => {
|
|
||||||
const sub = valuesObservable.subscribe((values) => {
|
|
||||||
expect(values).toContain(filterName1);
|
|
||||||
expect(values).toContain(filterName2);
|
|
||||||
expect(values).toContain(filterName3);
|
|
||||||
});
|
|
||||||
sub.unsubscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an observable that does not contain the non-existing filters', () => {
|
|
||||||
const sub = valuesObservable.subscribe((values) => {
|
|
||||||
expect(values).not.toContain(nonExistingFilter1);
|
|
||||||
expect(values).not.toContain(nonExistingFilter2);
|
|
||||||
});
|
|
||||||
sub.unsubscribe();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when isCollapsed is called and the filter is collapsed', () => {
|
describe('when isCollapsed is called and the filter is collapsed', () => {
|
||||||
let isActive: Observable<boolean>;
|
let isActive: Observable<boolean>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
filterService.isCollapsed = () => observableOf(true);
|
searchFilterService.isCollapsed = () => observableOf(true);
|
||||||
isActive = (comp as any).isCollapsed();
|
isActive = (comp as any).isCollapsed();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +110,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
describe('when isCollapsed is called and the filter is not collapsed', () => {
|
describe('when isCollapsed is called and the filter is not collapsed', () => {
|
||||||
let isActive: Observable<boolean>;
|
let isActive: Observable<boolean>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
filterService.isCollapsed = () => observableOf(false);
|
searchFilterService.isCollapsed = () => observableOf(false);
|
||||||
isActive = (comp as any).isCollapsed();
|
isActive = (comp as any).isCollapsed();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf, combineLatest, Subscription } from 'rxjs';
|
||||||
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SearchFilterConfig } from '../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../models/search-filter-config.model';
|
||||||
@@ -11,6 +11,10 @@ import { SearchService } from '../../../../core/shared/search/search.service';
|
|||||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SequenceService } from '../../../../core/shared/sequence.service';
|
import { SequenceService } from '../../../../core/shared/sequence.service';
|
||||||
|
import { FacetValues } from '../../models/facet-values.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { AppliedFilter } from '../../models/applied-filter.model';
|
||||||
|
import { SearchOptions } from '../../models/search-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filter',
|
selector: 'ds-search-filter',
|
||||||
@@ -22,7 +26,7 @@ import { SequenceService } from '../../../../core/shared/sequence.service';
|
|||||||
/**
|
/**
|
||||||
* Represents a part of the filter section for a single type of filter
|
* Represents a part of the filter section for a single type of filter
|
||||||
*/
|
*/
|
||||||
export class SearchFilterComponent implements OnInit {
|
export class SearchFilterComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* The filter config for this component
|
* The filter config for this component
|
||||||
*/
|
*/
|
||||||
@@ -61,13 +65,15 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Emits all currently selected values for this filter
|
* Emits all currently selected values for this filter
|
||||||
*/
|
*/
|
||||||
selectedValues$: Observable<string[]>;
|
appliedFilters$: Observable<AppliedFilter[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the current filter is supposed to be shown
|
* Emits true when the current filter is supposed to be shown
|
||||||
*/
|
*/
|
||||||
active$: Observable<boolean>;
|
active$: Observable<boolean>;
|
||||||
|
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
private readonly sequenceId: number;
|
private readonly sequenceId: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -85,15 +91,19 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
* Else, the filter should initially be collapsed
|
* Else, the filter should initially be collapsed
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.selectedValues$ = this.getSelectedValues();
|
this.appliedFilters$ = this.searchService.getSelectedValuesForFilter(this.filter.name);
|
||||||
this.active$ = this.isActive();
|
this.active$ = this.isActive();
|
||||||
this.collapsed$ = this.isCollapsed();
|
this.collapsed$ = this.isCollapsed();
|
||||||
this.initializeFilter();
|
this.initializeFilter();
|
||||||
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
this.subs.push(this.appliedFilters$.pipe(take(1)).subscribe((selectedValues: AppliedFilter[]) => {
|
||||||
if (isNotEmpty(selectedValues)) {
|
if (isNotEmpty(selectedValues)) {
|
||||||
this.filterService.expand(this.filter.name);
|
this.filterService.expand(this.filter.name);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,13 +128,6 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
this.filterService.initializeFilter(this.filter);
|
this.filterService.initializeFilter(this.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Observable<string[]>} Emits a list of all values that are currently active for this filter
|
|
||||||
*/
|
|
||||||
private getSelectedValues(): Observable<string[]> {
|
|
||||||
return this.filterService.getSelectedValuesForFilter(this.filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||||
* @param event The animation event
|
* @param event The animation event
|
||||||
@@ -164,20 +167,20 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||||
*/
|
*/
|
||||||
private isActive(): Observable<boolean> {
|
private isActive(): Observable<boolean> {
|
||||||
return this.selectedValues$.pipe(
|
return combineLatest([
|
||||||
switchMap((isActive) => {
|
this.appliedFilters$,
|
||||||
if (isNotEmpty(isActive)) {
|
this.searchConfigService.searchOptions,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([selectedValues, options]: [AppliedFilter[], SearchOptions]) => {
|
||||||
|
if (isNotEmpty(selectedValues)) {
|
||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
} else {
|
} else {
|
||||||
return this.searchConfigService.searchOptions.pipe(
|
|
||||||
switchMap((options) => {
|
|
||||||
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
|
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
|
||||||
filter((RD) => !RD.isLoading),
|
filter((RD: RemoteData<FacetValues>) => !RD.isLoading),
|
||||||
map((valuesRD) => {
|
map((valuesRD: RemoteData<FacetValues>) => {
|
||||||
return valuesRD.payload?.totalElements > 0;
|
return valuesRD.payload?.totalElements > 0;
|
||||||
}),);
|
}),
|
||||||
}
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
startWith(true));
|
startWith(true));
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, EventEmitter, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
|
||||||
import { of as observableOf, BehaviorSubject } from 'rxjs';
|
import { of as observableOf, BehaviorSubject } from 'rxjs';
|
||||||
@@ -26,6 +26,7 @@ import { SearchConfigurationServiceStub } from '../../../../testing/search-confi
|
|||||||
import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
import { AppliedFilter } from '../../../models/applied-filter.model';
|
import { AppliedFilter } from '../../../models/applied-filter.model';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
|
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
||||||
|
|
||||||
describe('SearchHierarchyFilterComponent', () => {
|
describe('SearchHierarchyFilterComponent', () => {
|
||||||
|
|
||||||
@@ -38,10 +39,7 @@ describe('SearchHierarchyFilterComponent', () => {
|
|||||||
select: new EventEmitter<VocabularyEntryDetail>(),
|
select: new EventEmitter<VocabularyEntryDetail>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchService = {
|
let searchService: SearchServiceStub;
|
||||||
getSearchLink: () => testSearchLink,
|
|
||||||
getFacetValuesFor: () => observableOf([]),
|
|
||||||
};
|
|
||||||
const searchFilterService = {
|
const searchFilterService = {
|
||||||
getPage: () => observableOf(0),
|
getPage: () => observableOf(0),
|
||||||
};
|
};
|
||||||
@@ -56,6 +54,8 @@ describe('SearchHierarchyFilterComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
searchService = new SearchServiceStub();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -77,7 +77,7 @@ describe('SearchHierarchyFilterComponent', () => {
|
|||||||
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
|
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
|
||||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, of as observableOf , Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith } from 'rxjs/operators';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { renderFacetFor } from '../search-filter-type-decorator';
|
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||||
@@ -19,8 +19,6 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-p
|
|||||||
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||||
import { RouteService } from '../../../../../core/services/route.service';
|
import { RouteService } from '../../../../../core/services/route.service';
|
||||||
import { hasValue } from '../../../../empty.util';
|
import { hasValue } from '../../../../empty.util';
|
||||||
import { AppliedFilter } from '../../../models/applied-filter.model';
|
|
||||||
import { FacetValues } from '../../../models/facet-values.model';
|
|
||||||
import { yearFromString } from 'src/app/shared/date.util';
|
import { yearFromString } from 'src/app/shared/date.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +47,7 @@ export const RANGE_FILTER_MAX_SUFFIX = '.max';
|
|||||||
* Component that represents a range facet for a specific filter configuration
|
* Component that represents a range facet for a specific filter configuration
|
||||||
*/
|
*/
|
||||||
@renderFacetFor(FilterType.range)
|
@renderFacetFor(FilterType.range)
|
||||||
export class SearchRangeFilterComponent extends SearchFacetFilterComponent implements OnInit, OnDestroy {
|
export class SearchRangeFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Fallback minimum for the range
|
* Fallback minimum for the range
|
||||||
*/
|
*/
|
||||||
@@ -65,11 +63,6 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
*/
|
*/
|
||||||
range: [number | undefined, number | undefined];
|
range: [number | undefined, number | undefined];
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to unsubscribe from
|
|
||||||
*/
|
|
||||||
sub: Subscription;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the sider is being controlled by the keyboard.
|
* Whether the sider is being controlled by the keyboard.
|
||||||
* Supresses any changes until the key is released.
|
* Supresses any changes until the key is released.
|
||||||
@@ -100,25 +93,13 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
this.max = yearFromString(this.filterConfig.maxValue) || this.max;
|
this.max = yearFromString(this.filterConfig.maxValue) || this.max;
|
||||||
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
|
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
|
||||||
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
||||||
this.sub = observableCombineLatest([iniMin, iniMax]).pipe(
|
this.subs.push(observableCombineLatest([iniMin, iniMax]).pipe(
|
||||||
map(([min, max]: [string, string]) => {
|
map(([min, max]: [string, string]) => {
|
||||||
const minimum = hasValue(min) ? Number(min) : this.min;
|
const minimum = hasValue(min) ? Number(min) : this.min;
|
||||||
const maximum = hasValue(max) ? Number(max) : this.max;
|
const maximum = hasValue(max) ? Number(max) : this.max;
|
||||||
return [minimum, maximum];
|
return [minimum, maximum];
|
||||||
})
|
})
|
||||||
).subscribe((minmax: [number, number]) => this.range = minmax);
|
).subscribe((minmax: [number, number]) => this.range = minmax));
|
||||||
}
|
|
||||||
|
|
||||||
setAppliedFilter(allFacetValues: FacetValues[]): void {
|
|
||||||
const appliedFilters: AppliedFilter[] = [].concat(...allFacetValues.map((facetValues: FacetValues) => facetValues.appliedFilters))
|
|
||||||
.filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter))
|
|
||||||
.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === this.filterConfig.name)
|
|
||||||
// TODO this should ideally be fixed in the backend
|
|
||||||
.map((appliedFilter: AppliedFilter) => Object.assign({}, appliedFilter, {
|
|
||||||
operator: 'range',
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.selectedAppliedFilters$ = observableOf(appliedFilters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,13 +140,4 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
return isPlatformBrowser(this.platformId);
|
return isPlatformBrowser(this.platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from all subscriptions
|
|
||||||
*/
|
|
||||||
ngOnDestroy() {
|
|
||||||
super.ngOnDestroy();
|
|
||||||
if (hasValue(this.sub)) {
|
|
||||||
this.sub.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@ import { SearchFilterConfig } from '../search/models/search-filter-config.model'
|
|||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
|
/**
|
||||||
|
* Stub class of {@link SearchFilterService}
|
||||||
|
*/
|
||||||
export class SearchFilterServiceStub {
|
export class SearchFilterServiceStub {
|
||||||
|
|
||||||
isFilterActiveWithValue(_paramName: string, _filterValue: string): Observable<boolean> {
|
isFilterActiveWithValue(_paramName: string, _filterValue: string): Observable<boolean> {
|
||||||
@@ -39,10 +42,6 @@ export class SearchFilterServiceStub {
|
|||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable<string[]> {
|
|
||||||
return observableOf([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
isCollapsed(_filterName: string): Observable<boolean> {
|
isCollapsed(_filterName: string): Observable<boolean> {
|
||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,11 @@ import {of as observableOf, Observable , BehaviorSubject } from 'rxjs';
|
|||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
import { SearchFilterConfig } from '../search/models/search-filter-config.model';
|
import { SearchFilterConfig } from '../search/models/search-filter-config.model';
|
||||||
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
|
||||||
|
import { AppliedFilter } from '../search/models/applied-filter.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link SearchService}
|
||||||
|
*/
|
||||||
export class SearchServiceStub {
|
export class SearchServiceStub {
|
||||||
|
|
||||||
private _viewMode: ViewMode;
|
private _viewMode: ViewMode;
|
||||||
@@ -14,6 +18,10 @@ export class SearchServiceStub {
|
|||||||
this.setViewMode(ViewMode.ListElement);
|
this.setViewMode(ViewMode.ListElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectedValuesForFilter(_filterName: string): Observable<AppliedFilter[]> {
|
||||||
|
return observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
getViewMode(): Observable<ViewMode> {
|
getViewMode(): Observable<ViewMode> {
|
||||||
return this.viewMode;
|
return this.viewMode;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user