Skip to content

Commit 1861166

Browse files
authored
Merge pull request DSpace#2814 from alexandrevryghem/use-applied-filter-to-display-label-on-search_contribute-main
Fixed authority ids being displayed on search page instead of their display value
2 parents a65f248 + b75f589 commit 1861166

55 files changed

Lines changed: 1112 additions & 785 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/core/services/route.service.spec.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import {
1515
hot,
1616
} from 'jasmine-marbles';
1717
import { of as observableOf } from 'rxjs';
18+
import { take } from 'rxjs/operators';
1819
import { TestScheduler } from 'rxjs/testing';
1920

2021
import { RouterMock } from '../../shared/mocks/router.mock';
22+
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
2123
import { AddUrlToHistoryAction } from '../history/history.actions';
2224
import { RouteService } from './route.service';
2325

@@ -40,23 +42,19 @@ describe('RouteService', () => {
4042
select: jasmine.createSpy('select'),
4143
});
4244

45+
let route: ActivatedRouteStub;
4346
const router = new RouterMock();
4447
router.setParams(convertToParamMap(paramObject));
4548

4649
paramObject[paramName1] = paramValue1;
4750
paramObject[paramName2] = [paramValue2a, paramValue2b];
4851

4952
beforeEach(waitForAsync(() => {
53+
route = new ActivatedRouteStub(paramObject);
54+
5055
return TestBed.configureTestingModule({
5156
providers: [
52-
{
53-
provide: ActivatedRoute,
54-
useValue: {
55-
queryParams: observableOf(paramObject),
56-
params: observableOf(paramObject),
57-
queryParamMap: observableOf(convertToParamMap(paramObject)),
58-
},
59-
},
57+
{ provide: ActivatedRoute, useValue: route },
6058
{ provide: Router, useValue: router },
6159
{ provide: Store, useValue: store },
6260
],
@@ -192,4 +190,51 @@ describe('RouteService', () => {
192190
});
193191
});
194192
});
193+
194+
describe('getParamsWithoutAppliedFilter', () => {
195+
beforeEach(() => {
196+
route.testParams = {
197+
'query': '',
198+
'spc.page': '1',
199+
'f.author': '1282121b-5394-4689-ab93-78d537764052,authority',
200+
'f.has_content_in_original_bundle': 'true,equals',
201+
};
202+
});
203+
204+
it('should remove the parameter completely if only one value is defined', (done: DoneFn) => {
205+
service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => {
206+
expect(params).toEqual({
207+
'query': '',
208+
'spc.page': '1',
209+
'f.has_content_in_original_bundle': 'true,equals',
210+
});
211+
done();
212+
});
213+
});
214+
215+
it('should remove the parameter completely if only one value is defined in an array', (done: DoneFn) => {
216+
route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,authority'];
217+
service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => {
218+
expect(params).toEqual({
219+
'query': '',
220+
'spc.page': '1',
221+
'f.has_content_in_original_bundle': 'true,equals',
222+
});
223+
done();
224+
});
225+
});
226+
227+
it('should return all params except the applied filter even when multiple filters of the same type are selected', (done: DoneFn) => {
228+
route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,authority', '71b91a28-c280-4352-a199-bd7fc3312501,authority'];
229+
service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => {
230+
expect(params).toEqual({
231+
'query': '',
232+
'spc.page': '1',
233+
'f.author': ['71b91a28-c280-4352-a199-bd7fc3312501,authority'],
234+
'f.has_content_in_original_bundle': 'true,equals',
235+
});
236+
done();
237+
});
238+
});
239+
});
195240
});

src/app/core/services/route.service.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,56 @@ export class RouteService {
249249
},
250250
);
251251
}
252+
253+
/**
254+
* Returns all the query parameters except for the one with the given name & value.
255+
*
256+
* @param name The name of the query param to exclude
257+
* @param value The optional value that the query param needs to have to be excluded
258+
*/
259+
getParamsExceptValue(name: string, value?: string): Observable<Params> {
260+
return this.route.queryParams.pipe(
261+
map((params: Params) => {
262+
const newParams: Params = Object.assign({}, params);
263+
const queryParamValues: string | string[] = newParams[name];
264+
265+
if (queryParamValues === value || value === undefined) {
266+
delete newParams[name];
267+
} else if (Array.isArray(queryParamValues) && queryParamValues.includes(value)) {
268+
newParams[name] = (queryParamValues as string[]).filter((paramValue: string) => paramValue !== value);
269+
if (newParams[name].length === 0) {
270+
delete newParams[name];
271+
}
272+
}
273+
return newParams;
274+
}),
275+
);
276+
}
277+
278+
/**
279+
* Returns all the existing query parameters and the new value pair with the given name & value.
280+
*
281+
* @param name The name of the query param for which you need to add the value
282+
* @param value The optional value that the query param needs to have in addition to the current ones
283+
*/
284+
getParamsWithAdditionalValue(name: string, value: string): Observable<Params> {
285+
return this.route.queryParams.pipe(
286+
map((params: Params) => {
287+
const newParams: Params = Object.assign({}, params);
288+
const queryParamValues: string | string[] = newParams[name];
289+
290+
if (queryParamValues === undefined) {
291+
newParams[name] = value;
292+
} else {
293+
if (Array.isArray(queryParamValues)) {
294+
newParams[name] = [...queryParamValues, value];
295+
} else {
296+
newParams[name] = [queryParamValues, value];
297+
}
298+
}
299+
return newParams;
300+
}),
301+
);
302+
}
303+
252304
}

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { map } from 'rxjs/operators';
99
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
1010
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
1111
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
12+
import { AppliedFilter } from '../../../shared/search/models/applied-filter.model';
1213
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
1314
import { SearchFilter } from '../../../shared/search/models/search-filter.model';
1415
import { SearchObjects } from '../../../shared/search/models/search-objects.model';
@@ -47,12 +48,13 @@ describe('SearchConfigurationService', () => {
4748
getQueryParameterValue: observableOf(value1),
4849
getQueryParamsWithPrefix: observableOf(prefixFilter),
4950
getRouteParameterValue: observableOf(''),
51+
getParamsExceptValue: observableOf({}),
5052
});
5153

5254
const paginationService = new PaginationServiceStub();
5355

5456

55-
const activatedRoute: any = new ActivatedRouteStub();
57+
const activatedRoute: ActivatedRouteStub = new ActivatedRouteStub();
5658
const linkService: any = {};
5759
const requestService: any = getMockRequestService();
5860
const halService: any = {
@@ -78,7 +80,7 @@ describe('SearchConfigurationService', () => {
7880
},
7981
};
8082
beforeEach(() => {
81-
service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute, linkService, halService, requestService, rdb);
83+
service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute as any, linkService, halService, requestService, rdb);
8284
});
8385

8486
describe('when the scope is called', () => {
@@ -287,4 +289,29 @@ describe('SearchConfigurationService', () => {
287289
expect((service as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
288290
});
289291
});
292+
293+
describe('unselectAppliedFilterParams', () => {
294+
let appliedFilter: AppliedFilter;
295+
296+
beforeEach(() => {
297+
appliedFilter = Object.assign(new AppliedFilter(), {
298+
filter: 'author',
299+
operator: 'authority',
300+
value: '1282121b-5394-4689-ab93-78d537764052',
301+
label: 'Odinson, Thor',
302+
});
303+
});
304+
305+
it('should return all params except the applied filter', () => {
306+
service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator);
307+
308+
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority');
309+
});
310+
311+
it('should be able to remove AppliedFilter without operator', () => {
312+
service.unselectAppliedFilterParams('dateIssued.max', '2000');
313+
314+
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000');
315+
});
316+
});
290317
});

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-
3333
import { SearchFilter } from '../../../shared/search/models/search-filter.model';
3434
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
3535
import { SearchOptions } from '../../../shared/search/models/search-options.model';
36+
import { addOperatorToFilterValue } from '../../../shared/search/search.utils';
3637
import { LinkService } from '../../cache/builders/link.service';
3738
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
3839
import {
@@ -556,6 +557,27 @@ export class SearchConfigurationService implements OnDestroy {
556557
);
557558
}
558559

560+
/**
561+
* Calculates the {@link Params} of the search after removing a filter with a certain value
562+
*
563+
* @param filterName The {@link AppliedFilter}'s name
564+
* @param value The {@link AppliedFilter}'s value
565+
* @param operator The {@link AppliedFilter}'s optional operator
566+
*/
567+
unselectAppliedFilterParams(filterName: string, value: string, operator?: string): Observable<Params> {
568+
return this.routeService.getParamsExceptValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value);
569+
}
570+
571+
/**
572+
* Calculates the {@link Params} of the search after removing a filter with a certain value
573+
*
574+
* @param filterName The {@link AppliedFilter}'s name
575+
* @param value The {@link AppliedFilter}'s value
576+
* @param operator The {@link AppliedFilter}'s optional operator
577+
*/
578+
selectNewAppliedFilterParams(filterName: string, value: string, operator?: string): Observable<Params> {
579+
return this.routeService.getParamsWithAdditionalValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value);
580+
}
559581

560582
/**
561583
* @returns {Observable<Params>} Emits the current view mode as a partial SearchOptions object

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
EventEmitter,
23
Injectable,
34
InjectionToken,
45
} from '@angular/core';
@@ -24,6 +25,7 @@ import {
2425
isNotEmpty,
2526
} from '../../../shared/empty.util';
2627
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
28+
import { AppliedFilter } from '../../../shared/search/models/applied-filter.model';
2729
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
2830
import {
2931
SearchFilterCollapseAction,
@@ -50,6 +52,7 @@ export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionTo
5052
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
5153
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
5254
export const SCOPE: InjectionToken<string> = new InjectionToken<string>('scope');
55+
export const CHANGE_APPLIED_FILTERS: InjectionToken<EventEmitter<AppliedFilter[]>> = new InjectionToken('changeAppliedFilters');
5356

5457
/**
5558
* Service that performs all actions that have to do with search filters and facets
@@ -84,15 +87,15 @@ export class SearchFilterService {
8487
* Fetch the current active scope from the query parameters
8588
* @returns {Observable<string>}
8689
*/
87-
getCurrentScope() {
90+
getCurrentScope(): Observable<string> {
8891
return this.routeService.getQueryParameterValue('scope');
8992
}
9093

9194
/**
9295
* Fetch the current query from the query parameters
9396
* @returns {Observable<string>}
9497
*/
95-
getCurrentQuery() {
98+
getCurrentQuery(): Observable<string> {
9699
return this.routeService.getQueryParameterValue('query');
97100
}
98101

@@ -134,15 +137,15 @@ export class SearchFilterService {
134137
* Fetch the current active filters from the query parameters
135138
* @returns {Observable<Params>}
136139
*/
137-
getCurrentFilters() {
140+
getCurrentFilters(): Observable<Params> {
138141
return this.routeService.getQueryParamsWithPrefix('f.');
139142
}
140143

141144
/**
142145
* Fetch the current view from the query parameters
143146
* @returns {Observable<string>}
144147
*/
145-
getCurrentView() {
148+
getCurrentView(): Observable<string> {
146149
return this.routeService.getQueryParameterValue('view');
147150
}
148151

src/app/shared/search-form/search-form.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</button>
1010
</div>
1111
<input type="text" [(ngModel)]="query" name="query" class="form-control"
12-
attr.aria-label="{{ searchPlaceholder }}" [attr.data-test]="'search-box' | dsBrowserOnly"
12+
[attr.aria-label]="searchPlaceholder" [attr.data-test]="'search-box' | dsBrowserOnly"
1313
[placeholder]="searchPlaceholder">
1414
<span class="input-group-append">
1515
<button type="submit" class="search-button btn btn-{{brandColor}}" [attr.data-test]="'search-button' | dsBrowserOnly"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { autoserialize } from 'cerialize';
2+
3+
export class AppliedFilter {
4+
5+
@autoserialize
6+
filter: string;
7+
8+
@autoserialize
9+
operator: string;
10+
11+
@autoserialize
12+
value: string;
13+
14+
@autoserialize
15+
label: string;
16+
17+
}

src/app/shared/search/models/search-query-response.model.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { autoserialize } from 'cerialize';
1+
import {
2+
autoserialize,
3+
autoserializeAs,
4+
} from 'cerialize';
25

36
import { PaginatedList } from '../../../core/data/paginated-list.model';
47
import { PageInfo } from '../../../core/shared/page-info.model';
8+
import { AppliedFilter } from './applied-filter.model';
9+
import { SearchResultSorting } from './search-result-sorting.model';
510

611
/**
712
* Class representing the response returned by the server when performing a search request
@@ -22,14 +27,14 @@ export abstract class SearchQueryResponse<T> extends PaginatedList<T> {
2227
/**
2328
* The currently active filters used in the search request
2429
*/
25-
@autoserialize
26-
appliedFilters: any[]; // TODO
30+
@autoserializeAs(AppliedFilter)
31+
appliedFilters: AppliedFilter[];
2732

2833
/**
2934
* The sort parameters used in the search request
3035
*/
31-
@autoserialize
32-
sort: any; // TODO
36+
@autoserializeAs(SearchResultSorting)
37+
sort: SearchResultSorting;
3338

3439
/**
3540
* The sort parameters used in the search request
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { autoserialize } from 'cerialize';
2+
3+
export class SearchResultSorting {
4+
5+
@autoserialize
6+
by: string;
7+
8+
@autoserialize
9+
order: string;
10+
11+
}

src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<div>
22
<div class="filters py-2">
3-
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
4-
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
3+
<ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
4+
<ng-container *ngFor="let page of (facetValues$ | async)">
55
<div [@facetLoad]="animationState">
6-
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
6+
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
77
</div>
88
</ng-container>
99
<div class="clearfix toggle-more-filters">

0 commit comments

Comments
 (0)