Skip to content

Commit 28088bc

Browse files
111731: Made the code completely dependent on the SearchService#appliedFilters$ instead of relying on the route query parameters
1 parent 58d7387 commit 28088bc

10 files changed

Lines changed: 94 additions & 227 deletions

File tree

src/app/core/shared/search/search-filter.service.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,6 @@ describe('SearchFilterService', () => {
153153
});
154154
});
155155

156-
describe('when the getSelectedValuesForFilter method is called', () => {
157-
beforeEach(() => {
158-
spyOn(routeServiceStub, 'getQueryParameterValues');
159-
service.getSelectedValuesForFilter(mockFilterConfig);
160-
});
161-
162-
it('should call getQueryParameterValues on the route service with the same parameters', () => {
163-
expect(routeServiceStub.getQueryParameterValues).toHaveBeenCalledWith(mockFilterConfig.paramName);
164-
});
165-
});
166-
167156
describe('when the getCurrentScope method is called', () => {
168157
beforeEach(() => {
169158
spyOn(routeServiceStub, 'getQueryParameterValue');

src/app/core/shared/search/search-filter.service.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,6 @@ export class SearchFilterService {
136136
return this.routeService.getQueryParameterValue('view');
137137
}
138138

139-
/**
140-
* Requests the active filter values set for a given filter
141-
* @param {SearchFilterConfig} filterConfig The configuration for which the filters are active
142-
* @returns {Observable<string[]>} Emits the active filters for the given filter configuration
143-
*/
144-
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
145-
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
146-
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
147-
map((params: Params) => [].concat(...Object.values(params))),
148-
);
149-
return observableCombineLatest(values$, prefixValues$).pipe(
150-
map(([values, prefixValues]) => {
151-
if (isNotEmpty(values)) {
152-
return values;
153-
}
154-
return prefixValues;
155-
}
156-
)
157-
);
158-
}
159-
160139
/**
161140
* Updates the found facet value suggestions for a given query
162141
* Transforms the found values into display values

src/app/core/shared/search/search.service.ts

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-classes-per-file */
22
import { combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
3-
import { Injectable, OnDestroy } from '@angular/core';
4-
import { map, switchMap, take, tap } from 'rxjs/operators';
3+
import { Injectable } from '@angular/core';
4+
import { map, switchMap, take, tap, distinctUntilChanged } from 'rxjs/operators';
55
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
66
import { ResponseParsingService } from '../../data/parsing.service';
77
import { RemoteData } from '../../data/remote-data';
@@ -61,7 +61,7 @@ class SearchDataService extends BaseDataService<any> {
6161
* Service that performs all general actions that have to do with the search page
6262
*/
6363
@Injectable()
64-
export class SearchService implements OnDestroy {
64+
export class SearchService {
6565

6666
/**
6767
* Endpoint link path for retrieving general search results
@@ -78,11 +78,6 @@ export class SearchService implements OnDestroy {
7878
*/
7979
private request: GenericConstructor<RestRequest> = GetRequest;
8080

81-
/**
82-
* Subscription to unsubscribe from
83-
*/
84-
private sub;
85-
8681
/**
8782
* Instance of SearchDataService to forward data service methods to
8883
*/
@@ -103,6 +98,18 @@ export class SearchService implements OnDestroy {
10398
this.searchDataService = new SearchDataService();
10499
}
105100

101+
/**
102+
* Get the currently {@link AppliedFilter}s for the given filter.
103+
*
104+
* @param filterName The name of the filter
105+
*/
106+
getSelectedValuesForFilter(filterName: string): Observable<AppliedFilter[]> {
107+
return this.appliedFilters$.pipe(
108+
map((appliedFilters: AppliedFilter[]) => appliedFilters.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === filterName)),
109+
distinctUntilChanged((previous: AppliedFilter[], next: AppliedFilter[]) => JSON.stringify(previous) === JSON.stringify(next)),
110+
);
111+
}
112+
106113
/**
107114
* Method to set service options
108115
* @param {GenericConstructor<ResponseParsingService>} parser The ResponseParsingService constructor name
@@ -176,26 +183,11 @@ export class SearchService implements OnDestroy {
176183
return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
177184
}
178185

179-
/**
180-
* Method to retrieve request entries for search results from the server
181-
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
182-
* @returns {Observable<RemoteData<SearchObjects<T>>>} Emits a paginated list with all search results found
183-
*/
184-
searchEntries<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<SearchObjects<T>>> {
185-
const href$ = this.getEndpoint(searchOptions);
186-
187-
const sqr$ = href$.pipe(
188-
switchMap((href: string) => this.rdb.buildFromHref<SearchObjects<T>>(href))
189-
);
190-
191-
return this.directlyAttachIndexableObjects(sqr$);
192-
}
193-
194186
/**
195187
* Method to directly attach the indexableObjects to search results, instead of using RemoteData.
196188
* For compatibility with the way the search was written originally
197189
*
198-
* @param sqr$: a SearchObjects RemotaData Observable without its
190+
* @param sqr$ A {@link SearchObjects} {@link RemoteData} Observable without its
199191
* indexableObjects attached
200192
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
201193
* no valid cached version. Defaults to true
@@ -384,12 +376,4 @@ export class SearchService implements OnDestroy {
384376
return '/search';
385377
}
386378

387-
/**
388-
* Unsubscribe from the subscription
389-
*/
390-
ngOnDestroy(): void {
391-
if (this.sub !== undefined) {
392-
this.sub.unsubscribe();
393-
}
394-
}
395379
}

src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
33
import { Router, Params } from '@angular/router';
44

55
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
6-
import { debounceTime, distinctUntilChanged, filter, map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
6+
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
77

88
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
99
import { hasNoValue, hasValue } from '../../../../empty.util';
@@ -17,7 +17,6 @@ import { InputSuggestion } from '../../../../input-suggestions/input-suggestions
1717
import { SearchOptions } from '../../../models/search-options.model';
1818
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
1919
import { currentPath } from '../../../../utils/route.utils';
20-
import { stripOperatorFromFilterValue } from '../../../search.utils';
2120
import { FacetValues } from '../../../models/facet-values.model';
2221
import { AppliedFilter } from '../../../models/applied-filter.model';
2322

@@ -103,14 +102,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
103102
this.searchOptions$ = this.searchConfigService.searchOptions;
104103
this.subs.push(
105104
this.searchOptions$.subscribe(() => this.updateFilterValueList()),
106-
this.refreshFilters.asObservable().pipe(
107-
filter((toRefresh: boolean) => toRefresh),
108-
// NOTE This is a workaround, otherwise retrieving filter values returns tha old cached response
109-
debounceTime((100)),
110-
mergeMap(() => this.retrieveFilterValues(false))
111-
).subscribe()
105+
this.retrieveFilterValues().subscribe(),
112106
);
113-
this.retrieveFilterValues().subscribe();
107+
this.selectedAppliedFilters$ = this.searchService.getSelectedValuesForFilter(this.filterConfig.name);
114108
}
115109

116110
/**
@@ -217,9 +211,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
217211
}
218212
}
219213

220-
protected retrieveFilterValues(useCachedVersionIfAvailable = true): Observable<FacetValues[]> {
214+
/**
215+
* Retrieves all the filter value suggestion pages that need to be displayed in the facet and combines it into one
216+
* list.
217+
*/
218+
protected retrieveFilterValues(): Observable<FacetValues[]> {
221219
return observableCombineLatest([this.searchOptions$, this.currentPage]).pipe(
222-
switchMap(([options, page]: [SearchOptions, number]) => this.searchService.getFacetValuesFor(this.filterConfig, page, options, null, useCachedVersionIfAvailable).pipe(
220+
switchMap(([options, page]: [SearchOptions, number]) => this.searchService.getFacetValuesFor(this.filterConfig, page, options).pipe(
223221
getFirstSucceededRemoteDataPayload(),
224222
tap((facetValues: FacetValues) => {
225223
this.isLastPage$.next(hasNoValue(facetValues?.next));
@@ -242,27 +240,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
242240
return filterValues;
243241
}),
244242
tap((allFacetValues: FacetValues[]) => {
245-
this.setAppliedFilter(allFacetValues);
246243
this.animationState = 'ready';
247244
this.facetValues$.next(allFacetValues);
248245
})
249246
);
250247
}
251248

252-
setAppliedFilter(allFacetValues: FacetValues[]): void {
253-
const allAppliedFilters: AppliedFilter[] = [].concat(...allFacetValues.map((facetValues: FacetValues) => facetValues.appliedFilters))
254-
.filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter));
255-
256-
this.selectedAppliedFilters$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
257-
map((selectedValues: string[]) => {
258-
const appliedFilters: AppliedFilter[] = selectedValues.map((value: string) => {
259-
return allAppliedFilters.find((appliedFilter: AppliedFilter) => appliedFilter.value === stripOperatorFromFilterValue(value));
260-
}).filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter));
261-
return appliedFilters;
262-
}),
263-
);
264-
}
265-
266249
/**
267250
* Prevent unnecessary rerendering
268251
*/

src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts

Lines changed: 18 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
1+
import { ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
22

33
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
44
import { RouterTestingModule } from '@angular/router/testing';
@@ -14,51 +14,28 @@ import { SearchConfigurationServiceStub } from '../../../testing/search-configur
1414
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
1515
import { SequenceService } from '../../../../core/shared/sequence.service';
1616
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
17+
import { SearchServiceStub } from '../../../testing/search-service.stub';
18+
import { SearchFilterServiceStub } from '../../../testing/search-filter-service.stub';
1719

1820
describe('SearchFilterComponent', () => {
1921
let comp: SearchFilterComponent;
2022
let fixture: ComponentFixture<SearchFilterComponent>;
2123
const filterName1 = 'test name';
22-
const filterName2 = 'test2';
23-
const filterName3 = 'another name3';
24-
const nonExistingFilter1 = 'non existing 1';
25-
const nonExistingFilter2 = 'non existing 2';
24+
2625
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
2726
name: filterName1,
2827
filterType: FilterType.text,
2928
hasFacets: false,
3029
isOpenByDefault: false
3130
});
32-
const mockFilterService = {
33-
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
34-
toggle: (filter) => {
35-
},
36-
collapse: (filter) => {
37-
},
38-
expand: (filter) => {
39-
},
40-
initializeFilter: (filter) => {
41-
},
42-
getSelectedValuesForFilter: (filter) => {
43-
return observableOf([filterName1, filterName2, filterName3]);
44-
},
45-
isFilterActive: (filter) => {
46-
return observableOf([filterName1, filterName2, filterName3].indexOf(filter) >= 0);
47-
},
48-
isCollapsed: (filter) => {
49-
return observableOf(true);
50-
}
51-
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
52-
53-
};
54-
let filterService;
31+
let searchFilterService: SearchFilterServiceStub;
5532
let sequenceService;
5633
const mockResults = observableOf(['test', 'data']);
57-
const searchServiceStub = {
58-
getFacetValuesFor: (filter) => mockResults
59-
};
34+
let searchService: SearchServiceStub;
6035

6136
beforeEach(waitForAsync(() => {
37+
searchFilterService = new SearchFilterServiceStub();
38+
searchService = new SearchServiceStub();
6239
sequenceService = jasmine.createSpyObj('sequenceService', { next: 17 });
6340

6441
TestBed.configureTestingModule({
@@ -68,26 +45,23 @@ describe('SearchFilterComponent', () => {
6845
BrowserOnlyMockPipe,
6946
],
7047
providers: [
71-
{ provide: SearchService, useValue: searchServiceStub },
72-
{
73-
provide: SearchFilterService,
74-
useValue: mockFilterService
75-
},
48+
{ provide: SearchService, useValue: searchService },
49+
{ provide: SearchFilterService, useValue: searchFilterService },
7650
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
7751
{ provide: SequenceService, useValue: sequenceService },
7852
],
79-
schemas: [NO_ERRORS_SCHEMA]
53+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
8054
}).overrideComponent(SearchFilterComponent, {
8155
set: { changeDetection: ChangeDetectionStrategy.Default }
8256
}).compileComponents();
8357
}));
8458

8559
beforeEach(() => {
60+
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockResults);
8661
fixture = TestBed.createComponent(SearchFilterComponent);
8762
comp = fixture.componentInstance; // SearchPageComponent test instance
8863
comp.filter = mockFilterConfig;
8964
fixture.detectChanges();
90-
filterService = (comp as any).filterService;
9165
});
9266

9367
it('should generate unique IDs', () => {
@@ -98,54 +72,30 @@ describe('SearchFilterComponent', () => {
9872

9973
describe('when the toggle method is triggered', () => {
10074
beforeEach(() => {
101-
spyOn(filterService, 'toggle');
75+
spyOn(searchFilterService, 'toggle');
10276
comp.toggle();
10377
});
10478

10579
it('should call toggle with the correct filter configuration name', () => {
106-
expect(filterService.toggle).toHaveBeenCalledWith(mockFilterConfig.name);
80+
expect(searchFilterService.toggle).toHaveBeenCalledWith(mockFilterConfig.name);
10781
});
10882
});
10983

11084
describe('when the initializeFilter method is triggered', () => {
11185
beforeEach(() => {
112-
spyOn(filterService, 'initializeFilter');
86+
spyOn(searchFilterService, 'initializeFilter');
11387
comp.initializeFilter();
11488
});
11589

11690
it('should call initialCollapse with the correct filter configuration name', () => {
117-
expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig);
118-
});
119-
});
120-
121-
describe('when getSelectedValues is called', () => {
122-
let valuesObservable: Observable<string[]>;
123-
beforeEach(() => {
124-
valuesObservable = (comp as any).getSelectedValues();
125-
});
126-
127-
it('should return an observable containing the existing filters', () => {
128-
const sub = valuesObservable.subscribe((values) => {
129-
expect(values).toContain(filterName1);
130-
expect(values).toContain(filterName2);
131-
expect(values).toContain(filterName3);
132-
});
133-
sub.unsubscribe();
134-
});
135-
136-
it('should return an observable that does not contain the non-existing filters', () => {
137-
const sub = valuesObservable.subscribe((values) => {
138-
expect(values).not.toContain(nonExistingFilter1);
139-
expect(values).not.toContain(nonExistingFilter2);
140-
});
141-
sub.unsubscribe();
91+
expect(searchFilterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig);
14292
});
14393
});
14494

14595
describe('when isCollapsed is called and the filter is collapsed', () => {
14696
let isActive: Observable<boolean>;
14797
beforeEach(() => {
148-
filterService.isCollapsed = () => observableOf(true);
98+
searchFilterService.isCollapsed = () => observableOf(true);
14999
isActive = (comp as any).isCollapsed();
150100
});
151101

@@ -160,7 +110,7 @@ describe('SearchFilterComponent', () => {
160110
describe('when isCollapsed is called and the filter is not collapsed', () => {
161111
let isActive: Observable<boolean>;
162112
beforeEach(() => {
163-
filterService.isCollapsed = () => observableOf(false);
113+
searchFilterService.isCollapsed = () => observableOf(false);
164114
isActive = (comp as any).isCollapsed();
165115
});
166116

0 commit comments

Comments
 (0)