111731: Moved common remove/add queryParam logic for facet option/label to routeService

This commit is contained in:
Alexandre Vryghem
2024-02-13 11:36:26 +01:00
parent dca62d6eb1
commit 47f89a6f9d
24 changed files with 436 additions and 416 deletions

View File

@@ -9,6 +9,8 @@ import { RouteService } from './route.service';
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { AddUrlToHistoryAction } from '../history/history.actions'; import { AddUrlToHistoryAction } from '../history/history.actions';
import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
import { take } from 'rxjs/operators';
describe('RouteService', () => { describe('RouteService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -29,6 +31,7 @@ describe('RouteService', () => {
select: jasmine.createSpy('select') select: jasmine.createSpy('select')
}); });
let route: ActivatedRouteStub;
const router = new RouterMock(); const router = new RouterMock();
router.setParams(convertToParamMap(paramObject)); router.setParams(convertToParamMap(paramObject));
@@ -36,16 +39,11 @@ describe('RouteService', () => {
paramObject[paramName2] = [paramValue2a, paramValue2b]; paramObject[paramName2] = [paramValue2a, paramValue2b];
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
route = new ActivatedRouteStub(paramObject);
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
providers: [ providers: [
{ { provide: ActivatedRoute, useValue: route },
provide: ActivatedRoute,
useValue: {
queryParams: observableOf(paramObject),
params: observableOf(paramObject),
queryParamMap: observableOf(convertToParamMap(paramObject))
},
},
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: Store, useValue: store }, { provide: Store, useValue: store },
] ]
@@ -181,4 +179,39 @@ describe('RouteService', () => {
}); });
}); });
}); });
describe('getParamsWithoutAppliedFilter', () => {
beforeEach(() => {
route.testParams = {
'query': '',
'spc.page': '1',
'f.author': '1282121b-5394-4689-ab93-78d537764052,authority',
'f.has_content_in_original_bundle': 'true,equals',
};
});
it('should remove the parameter completely if only one value is defined', (done: DoneFn) => {
service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual({
'query': '',
'spc.page': '1',
'f.has_content_in_original_bundle': 'true,equals',
});
done();
});
});
it('should return all params except the applied filter even when multiple filters of the same type are selected', (done: DoneFn) => {
route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,authority', '71b91a28-c280-4352-a199-bd7fc3312501,authority'];
service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual({
'query': '',
'spc.page': '1',
'f.author': ['71b91a28-c280-4352-a199-bd7fc3312501,authority'],
'f.has_content_in_original_bundle': 'true,equals',
});
done();
});
});
});
}); });

View File

@@ -225,4 +225,53 @@ export class RouteService {
} }
); );
} }
/**
* Returns all the query parameters except for the one with the given name & value.
*
* @param name The name of the query param to exclude
* @param value The optional value that the query param needs to have to be excluded
*/
getParamsExceptValue(name: string, value?: string): Observable<Params> {
return this.route.queryParams.pipe(
map((params: Params) => {
const newParams: Params = Object.assign({}, params);
const queryParamValues: string | string[] = newParams[name];
if (queryParamValues === value || value === undefined) {
delete newParams[name];
} else if (Array.isArray(queryParamValues) && queryParamValues.includes(value)) {
newParams[name] = (queryParamValues as string[]).filter((paramValue: string) => paramValue !== value);
}
return newParams;
}),
);
}
/**
* Returns all the existing query parameters and the new value pair with the given name & value.
*
* @param name The name of the query param for which you need to add the value
* @param value The optional value that the query param needs to have in addition to the current ones
*/
getParamsWithAdditionalValue(name: string, value: string): Observable<Params> {
return this.route.queryParams.pipe(
map((params: Params) => {
const newParams: Params = Object.assign({}, params);
const queryParamValues: string | string[] = newParams[name];
if (queryParamValues === undefined) {
newParams[name] = value;
} else {
if (Array.isArray(queryParamValues)) {
newParams[name] = [...queryParamValues, value];
} else {
newParams[name] = [queryParamValues, value];
}
}
return newParams;
}),
);
}
} }

View File

@@ -13,8 +13,6 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { SearchObjects } from '../../../shared/search/models/search-objects.model'; import { SearchObjects } from '../../../shared/search/models/search-objects.model';
import { Params } from '@angular/router';
import { addOperatorToFilterValue } from '../../../shared/search/search.utils';
import { AppliedFilter } from '../../../shared/search/models/applied-filter.model'; import { AppliedFilter } from '../../../shared/search/models/applied-filter.model';
describe('SearchConfigurationService', () => { describe('SearchConfigurationService', () => {
@@ -41,7 +39,8 @@ describe('SearchConfigurationService', () => {
const routeService = jasmine.createSpyObj('RouteService', { const routeService = jasmine.createSpyObj('RouteService', {
getQueryParameterValue: observableOf(value1), getQueryParameterValue: observableOf(value1),
getQueryParamsWithPrefix: observableOf(prefixFilter), getQueryParamsWithPrefix: observableOf(prefixFilter),
getRouteParameterValue: observableOf('') getRouteParameterValue: observableOf(''),
getParamsExceptValue: observableOf({}),
}); });
const paginationService = new PaginationServiceStub(); const paginationService = new PaginationServiceStub();
@@ -283,7 +282,7 @@ describe('SearchConfigurationService', () => {
}); });
}); });
describe('getParamsWithoutAppliedFilter', () => { describe('unselectAppliedFilterParams', () => {
let appliedFilter: AppliedFilter; let appliedFilter: AppliedFilter;
beforeEach(() => { beforeEach(() => {
@@ -293,51 +292,18 @@ describe('SearchConfigurationService', () => {
value: '1282121b-5394-4689-ab93-78d537764052', value: '1282121b-5394-4689-ab93-78d537764052',
label: 'Odinson, Thor', label: 'Odinson, Thor',
}); });
activatedRoute.testParams = {
'query': '',
'spc.page': '1',
'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator),
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
'f.dateIssued.max': '2000',
};
}); });
it('should return all params except the applied filter', (done: DoneFn) => { it('should return all params except the applied filter', () => {
service.getParamsWithoutAppliedFilter(appliedFilter.filter, appliedFilter.value, appliedFilter.operator).pipe(take(1)).subscribe((params: Params) => { service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator);
expect(params).toEqual({
'query': '', expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority');
'spc.page': '1',
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
'f.dateIssued.max': '2000',
});
done();
});
}); });
it('should return all params except the applied filter even when multiple filters of the same type are selected', (done: DoneFn) => { it('should be able to remove AppliedFilter without operator', () => {
activatedRoute.testParams['f.author'] = [addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator), addOperatorToFilterValue('71b91a28-c280-4352-a199-bd7fc3312501', 'authority')]; service.unselectAppliedFilterParams('dateIssued.max', '2000');
service.getParamsWithoutAppliedFilter(appliedFilter.filter, appliedFilter.value, appliedFilter.operator).pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual({
'query': '',
'spc.page': '1',
'f.author': [addOperatorToFilterValue('71b91a28-c280-4352-a199-bd7fc3312501', 'authority')],
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
'f.dateIssued.max': '2000',
});
done();
});
});
it('should be able to remove AppliedFilter without operator', (done: DoneFn) => { expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000');
service.getParamsWithoutAppliedFilter('dateIssued.max', '2000').pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual({
'query': '',
'spc.page': '1',
'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator),
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
});
done();
});
}); });
}); });
}); });

View File

@@ -526,22 +526,26 @@ export class SearchConfigurationService implements OnDestroy {
); );
} }
getParamsWithoutAppliedFilter(filterName: string, value: string, operator?: string): Observable<Params> { /**
return this.route.queryParams.pipe( * Calculates the {@link Params} of the search after removing a filter with a certain value
map((params: Params) => { *
const newParams: Params = Object.assign({}, params); * @param filterName The {@link AppliedFilter}'s name
const queryParamValues: string | string[] = newParams[`f.${filterName}`]; * @param value The {@link AppliedFilter}'s value
const excludeValue = hasValue(operator) ? addOperatorToFilterValue(value, operator) : value; * @param operator The {@link AppliedFilter}'s optional operator
*/
if (queryParamValues === excludeValue) { unselectAppliedFilterParams(filterName: string, value: string, operator?: string): Observable<Params> {
delete newParams[`f.${filterName}`]; return this.routeService.getParamsExceptValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value);
} else if (queryParamValues?.includes(excludeValue)) {
newParams[`f.${filterName}`] = (queryParamValues as string[])
.filter((paramValue: string) => paramValue !== excludeValue);
} }
return newParams;
}), /**
); * Calculates the {@link Params} of the search after removing a filter with a certain value
*
* @param filterName The {@link AppliedFilter}'s name
* @param value The {@link AppliedFilter}'s value
* @param operator The {@link AppliedFilter}'s optional operator
*/
selectNewAppliedFilterParams(filterName: string, value: string, operator?: string): Observable<Params> {
return this.routeService.getParamsWithAdditionalValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value);
} }
/** /**

View File

@@ -63,7 +63,7 @@ export class SearchFilterService {
* Fetch the current active scope from the query parameters * Fetch the current active scope from the query parameters
* @returns {Observable<string>} * @returns {Observable<string>}
*/ */
getCurrentScope() { getCurrentScope(): Observable<string> {
return this.routeService.getQueryParameterValue('scope'); return this.routeService.getQueryParameterValue('scope');
} }
@@ -71,7 +71,7 @@ export class SearchFilterService {
* Fetch the current query from the query parameters * Fetch the current query from the query parameters
* @returns {Observable<string>} * @returns {Observable<string>}
*/ */
getCurrentQuery() { getCurrentQuery(): Observable<string> {
return this.routeService.getQueryParameterValue('query'); return this.routeService.getQueryParameterValue('query');
} }
@@ -113,7 +113,7 @@ export class SearchFilterService {
* Fetch the current active filters from the query parameters * Fetch the current active filters from the query parameters
* @returns {Observable<Params>} * @returns {Observable<Params>}
*/ */
getCurrentFilters() { getCurrentFilters(): Observable<Params> {
return this.routeService.getQueryParamsWithPrefix('f.'); return this.routeService.getQueryParamsWithPrefix('f.');
} }
@@ -121,7 +121,7 @@ export class SearchFilterService {
* Fetch the current view from the query parameters * Fetch the current view from the query parameters
* @returns {Observable<string>} * @returns {Observable<string>}
*/ */
getCurrentView() { getCurrentView(): Observable<string> {
return this.routeService.getQueryParameterValue('view'); return this.routeService.getQueryParameterValue('view');
} }

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="filters py-2"> <div class="filters py-2">
<ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option> <ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
<ng-container *ngFor="let page of (facetValues$ | async)"> <ng-container *ngFor="let page of (facetValues$ | async)">
<div [@facetLoad]="animationState"> <div [@facetLoad]="animationState">
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option> <ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
</div> </div>
</ng-container> </ng-container>
<div class="clearfix toggle-more-filters"> <div class="clearfix toggle-more-filters">

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="filters py-2"> <div class="filters py-2">
<ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option> <ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
<ng-container *ngFor="let page of (facetValues$ | async)"> <ng-container *ngFor="let page of (facetValues$ | async)">
<div [@facetLoad]="animationState"> <div [@facetLoad]="animationState">
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option> <ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
</div> </div>
</ng-container> </ng-container>
<div class="clearfix toggle-more-filters"> <div class="clearfix toggle-more-filters">

View File

@@ -1,7 +1,7 @@
<a *ngIf="isVisible | async" class="d-flex flex-row" <a *ngIf="isVisible | async" class="d-flex flex-row"
[tabIndex]="-1" [tabIndex]="-1"
[routerLink]="[searchLink]" [routerLink]="[searchLink]"
[queryParams]="addQueryParams" queryParamsHandling="merge"> [queryParams]="addQueryParams$ | async">
<label class="mb-0"> <label class="mb-0">
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/> <input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
<span class="filter-value px-1"> <span class="filter-value px-1">

View File

@@ -3,9 +3,9 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router'; import { Router, Params } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf, take } from 'rxjs';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service'; import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service';
import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../../core/shared/search/search.service';
@@ -18,15 +18,14 @@ import { SearchFacetOptionComponent } from './search-facet-option.component';
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { SearchConfigurationServiceStub } from '../../../../../testing/search-configuration-service.stub';
import { SearchFilterServiceStub } from '../../../../../testing/search-filter-service.stub';
describe('SearchFacetOptionComponent', () => { describe('SearchFacetOptionComponent', () => {
let comp: SearchFacetOptionComponent; let comp: SearchFacetOptionComponent;
let fixture: ComponentFixture<SearchFacetOptionComponent>; let fixture: ComponentFixture<SearchFacetOptionComponent>;
const filterName1 = 'testname'; const filterName1 = 'testname';
const filterName2 = 'testAuthorityname';
const value1 = 'testvalue1';
const value2 = 'test2'; const value2 = 'test2';
const operator = 'authority';
const mockFilterConfig = Object.assign(new SearchFilterConfig(), { const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
name: filterName1, name: filterName1,
@@ -38,15 +37,7 @@ describe('SearchFacetOptionComponent', () => {
maxValue: 3000, maxValue: 3000,
}); });
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), { const facetValue: FacetValue = {
name: filterName2,
filterType: FilterType.authority,
hasFacets: false,
isOpenByDefault: false,
pageSize: 2
});
const value: FacetValue = {
label: value2, label: value2,
value: value2, value: value2,
count: 20, count: 20,
@@ -56,63 +47,30 @@ describe('SearchFacetOptionComponent', () => {
} }
}; };
const selectedValue: FacetValue = {
label: value1,
value: value1,
count: 20,
_links: {
self: { href: 'selectedValue-self-link1' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1},${operator}` }
}
};
const authorityValue: FacetValue = {
label: value2,
value: value2,
count: 20,
_links: {
self: { href: 'authorityValue-self-link2' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` }
}
};
const searchLink = '/search'; const searchLink = '/search';
const selectedValues = [selectedValue]; let searchConfigurationService: SearchConfigurationServiceStub;
const selectedValues$ = observableOf(selectedValues); let searchFilterService: SearchFilterServiceStub;
let filterService; let searchService: SearchServiceStub;
let searchService; let router: RouterStub;
let router;
const page = observableOf(0);
const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); const pagination = Object.assign(new PaginationComponentOptions(), { id: 'test-id', currentPage: 1, pageSize: 20 });
const paginationService = new PaginationServiceStub(pagination); const paginationService = new PaginationServiceStub(pagination);
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ searchConfigurationService = new SearchConfigurationServiceStub();
searchFilterService = new SearchFilterServiceStub();
searchService = new SearchServiceStub(searchLink);
router = new RouterStub();
void TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
declarations: [SearchFacetOptionComponent], declarations: [SearchFacetOptionComponent],
providers: [ providers: [
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: searchService },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: router },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ { provide: SearchConfigurationService, useValue: searchConfigurationService },
provide: SearchConfigurationService, useValue: { { provide: SearchFilterService, useValue: searchFilterService },
paginationID: 'page-id',
searchOptions: observableOf({})
}
},
{
provide: SearchFilterService, useValue: {
getSelectedValuesForFilter: () => selectedValues,
isFilterActiveWithValue: (paramName: string, filterValue: string) => observableOf(true),
getPage: (paramName: string) => page,
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
incrementPage: (filterName: string) => {
},
resetPage: (filterName: string) => {
}
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}
}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchFacetOptionComponent, { }).overrideComponent(SearchFacetOptionComponent, {
@@ -123,37 +81,24 @@ describe('SearchFacetOptionComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SearchFacetOptionComponent); fixture = TestBed.createComponent(SearchFacetOptionComponent);
comp = fixture.componentInstance; // SearchPageComponent test instance comp = fixture.componentInstance; // SearchPageComponent test instance
filterService = (comp as any).filterService; comp.filterValue = facetValue;
searchService = (comp as any).searchService;
router = (comp as any).router;
comp.filterValue = value;
comp.selectedValues$ = selectedValues$;
comp.filterConfig = mockFilterConfig; comp.filterConfig = mockFilterConfig;
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('when the updateAddParams method is called with a value', () => { describe('updateAddParams', () => {
it('should update the addQueryParams with the new parameter values', () => { it('should always reset the page to 1', (done: DoneFn) => {
comp.addQueryParams = {}; spyOn(searchConfigurationService, 'selectNewAppliedFilterParams').and.returnValue(observableOf({
(comp as any).updateAddParams(selectedValues); [mockFilterConfig.paramName]: [`${facetValue.value},equals`],
expect(comp.addQueryParams).toEqual({ ['test-id.page']: 5,
[mockFilterConfig.paramName]: [`${value1},${operator}`, value.value + ',equals'], }));
['page-id.page']: 1
});
});
});
describe('when filter type is authority and the updateAddParams method is called with a value', () => { comp.updateAddParams().pipe(take(1)).subscribe((params: Params) => {
it('should update the addQueryParams with the new parameter values', () => { expect(params).toEqual({
comp.filterValue = authorityValue; [mockFilterConfig.paramName]: [`${facetValue.value},equals`],
comp.filterConfig = mockAuthorityFilterConfig; ['test-id.page']: 1,
fixture.detectChanges(); });
done();
comp.addQueryParams = {};
(comp as any).updateAddParams(selectedValues);
expect(comp.addQueryParams).toEqual({
[mockAuthorityFilterConfig.paramName]: [value1 + ',equals', `${value2},${operator}`],
['page-id.page']: 1
}); });
}); });
}); });

View File

@@ -1,13 +1,12 @@
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { FacetValue } from '../../../../models/facet-value.model'; import { FacetValue } from '../../../../models/facet-value.model';
import { SearchFilterConfig } from '../../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../../models/search-filter-config.model';
import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../../core/shared/search/search.service';
import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service'; import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { hasValue } from '../../../../../empty.util';
import { currentPath } from '../../../../../utils/route.utils'; import { currentPath } from '../../../../../utils/route.utils';
import { getFacetValueForType } from '../../../../search.utils'; import { getFacetValueForType } from '../../../../search.utils';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
@@ -21,7 +20,7 @@ import { PaginationService } from '../../../../../../core/pagination/pagination.
/** /**
* Represents a single option in a filter facet * Represents a single option in a filter facet
*/ */
export class SearchFacetOptionComponent implements OnInit, OnDestroy { export class SearchFacetOptionComponent implements OnInit {
/** /**
* A single value for this component * A single value for this component
*/ */
@@ -32,15 +31,10 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
*/ */
@Input() filterConfig: SearchFilterConfig; @Input() filterConfig: SearchFilterConfig;
/**
* Emits the active values for this filter
*/
@Input() selectedValues$: Observable<FacetValue[]>;
/** /**
* True when the search component should show results on the current page * True when the search component should show results on the current page
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch: boolean;
/** /**
* Emits true when this option should be visible and false when it should be invisible * Emits true when this option should be visible and false when it should be invisible
@@ -50,16 +44,12 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
/** /**
* UI parameters when this filter is added * UI parameters when this filter is added
*/ */
addQueryParams; addQueryParams$: Observable<Params>;
/** /**
* Link to the search page * Link to the search page
*/ */
searchLink: string; searchLink: string;
/**
* Subscription to unsubscribe from on destroy
*/
sub: Subscription;
paginationId: string; paginationId: string;
@@ -78,23 +68,20 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
this.paginationId = this.searchConfigService.paginationID; this.paginationId = this.searchConfigService.paginationID;
this.searchLink = this.getSearchLink(); this.searchLink = this.getSearchLink();
this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked)); this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked));
this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions) this.addQueryParams$ = this.updateAddParams();
.subscribe(([selectedValues, searchOptions]) => {
this.updateAddParams(selectedValues);
});
} }
/** /**
* Checks if a value for this filter is currently active * Checks if a value for this filter is currently active
*/ */
private isChecked(): Observable<boolean> { isChecked(): Observable<boolean> {
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue()); return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue());
} }
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
private getSearchLink(): string { getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return currentPath(this.router); return currentPath(this.router);
} }
@@ -102,31 +89,23 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
} }
/** /**
* Calculates the parameters that should change if a given value for this filter would be added to the active filters * Calculates the parameters that should change if this {@link filterValue} would be added to the active filters
* @param {string[]} selectedValues The values that are currently selected for this filter
*/ */
private updateAddParams(selectedValues: FacetValue[]): void { updateAddParams(): Observable<Params> {
const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); const page: string = this.paginationService.getPageParam(this.searchConfigService.paginationID);
this.addQueryParams = { return this.searchConfigService.selectNewAppliedFilterParams(this.filterConfig.name, this.getFacetValue()).pipe(
[this.filterConfig.paramName]: [...selectedValues.map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), this.getFacetValue()], map((params: Params) => ({
[page]: 1 ...params,
}; [page]: 1,
})),
);
} }
/** /**
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
* Retrieve facet value related to facet type * Retrieve facet value related to facet type
*/ */
private getFacetValue(): string { getFacetValue(): string {
return getFacetValueForType(this.filterValue, this.filterConfig); return getFacetValueForType(this.filterValue, this.filterConfig);
} }
/**
* Make sure the subscription is unsubscribed from when this component is destroyed
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
} }

View File

@@ -1,7 +1,7 @@
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { FacetValue } from '../../../../models/facet-value.model'; import { FacetValue } from '../../../../models/facet-value.model';
import { SearchFilterConfig } from '../../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../../models/search-filter-config.model';
import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../../core/shared/search/search.service';
@@ -41,7 +41,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
/** /**
* True when the search component should show results on the current page * True when the search component should show results on the current page
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch: boolean;
/** /**
* Emits true when this option should be visible and false when it should be invisible * Emits true when this option should be visible and false when it should be invisible
@@ -51,7 +51,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
/** /**
* UI parameters when this filter is changed * UI parameters when this filter is changed
*/ */
changeQueryParams; changeQueryParams: Params;
/** /**
* Subscription to unsubscribe from on destroy * Subscription to unsubscribe from on destroy
@@ -109,7 +109,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); const page = this.paginationService.getPageParam(this.searchConfigService.paginationID);
this.changeQueryParams = { this.changeQueryParams = {
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min], [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min],
[this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: max === this.absoluteMax ? null : [max], [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: max === new Date().getUTCFullYear() ? null : [max],
[page]: 1 [page]: 1
}; };
} }

View File

@@ -1,7 +1,7 @@
<a class="d-flex flex-row" <a class="d-flex flex-row"
[tabIndex]="-1" [tabIndex]="-1"
[routerLink]="[searchLink]" [routerLink]="[searchLink]"
[queryParams]="removeQueryParams" queryParamsHandling="merge"> [queryParams]="removeQueryParams | async">
<label class="mb-0"> <label class="mb-0">
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/> <input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
<span class="filter-value pl-1 text-capitalize"> <span class="filter-value pl-1 text-capitalize">

View File

@@ -1,31 +1,31 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf, take } from 'rxjs';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service'; import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service';
import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../../core/shared/search/search.service';
import { RouterStub } from '../../../../../testing/router.stub'; import { RouterStub } from '../../../../../testing/router.stub';
import { SearchServiceStub } from '../../../../../testing/search-service.stub'; import { SearchServiceStub } from '../../../../../testing/search-service.stub';
import { FacetValue } from '../../../../models/facet-value.model';
import { FilterType } from '../../../../models/filter-type.model'; import { FilterType } from '../../../../models/filter-type.model';
import { SearchFilterConfig } from '../../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../../models/search-filter-config.model';
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component'; import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { AppliedFilter } from '../../../../models/applied-filter.model';
import { SearchConfigurationServiceStub } from '../../../../../testing/search-configuration-service.stub';
import { SearchFilterServiceStub } from '../../../../../testing/search-filter-service.stub';
describe('SearchFacetSelectedOptionComponent', () => { describe('SearchFacetSelectedOptionComponent', () => {
let comp: SearchFacetSelectedOptionComponent; let comp: SearchFacetSelectedOptionComponent;
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>; let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
const filterName1 = 'test name'; const filterName1 = 'test name';
const filterName2 = 'testAuthorityname'; const filterName2 = 'testAuthorityname';
const label1 = 'test value 1';
const value1 = 'testvalue1'; const value1 = 'testvalue1';
const label2 = 'test 2';
const value2 = 'test2'; const value2 = 'test2';
const operator = 'authority'; const operator = 'authority';
const mockFilterConfig = Object.assign(new SearchFilterConfig(), { const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
@@ -37,149 +37,63 @@ describe('SearchFacetSelectedOptionComponent', () => {
minValue: 200, minValue: 200,
maxValue: 3000, maxValue: 3000,
}); });
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), {
name: filterName2,
filterType: FilterType.authority,
hasFacets: false,
isOpenByDefault: false,
pageSize: 2
});
const searchLink = '/search'; const searchLink = '/search';
const selectedValue: FacetValue = { const appliedFilter: AppliedFilter = Object.assign(new AppliedFilter(), {
label: value1, filter: filterName2,
value: value1, operator: operator,
count: 20,
_links: {
self: { href: 'selectedValue-self-link1' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1}` }
}
};
const selectedValue2: FacetValue = {
label: value2, label: value2,
value: value2, value: value2,
count: 20, });
_links: { let searchService: SearchServiceStub;
self: { href: 'selectedValue-self-link2' }, let searchConfigurationService: SearchConfigurationServiceStub;
search: { href: `http://test.org/api/discover/search/objects?f.${filterName1}=${value2}` } let searchFilterService: SearchFilterServiceStub;
} let router: RouterStub;
};
const selectedAuthorityValue: FacetValue = {
label: label1,
value: value1,
count: 20,
_links: {
self: { href: 'selectedAuthorityValue-self-link1' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value1},${operator}` }
}
};
const selectedAuthorityValue2: FacetValue = {
label: label2,
value: value2,
count: 20,
_links: {
self: { href: 'selectedAuthorityValue-self-link2' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` }
}
};
const selectedValues = [selectedValue, selectedValue2];
const selectedAuthorityValues = [selectedAuthorityValue, selectedAuthorityValue2];
const facetValue = {
label: value2,
value: value2,
count: 1,
_links: {
self: { href: 'facetValue-self-link2' },
search: { href: `` }
}
};
const authorityValue: FacetValue = {
label: label2,
value: value2,
count: 20,
_links: {
self: { href: 'authorityValue-self-link2' },
search: { href: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}` }
}
};
const selectedValues$ = observableOf(selectedValues);
const selectedAuthorityValues$ = observableOf(selectedAuthorityValues);
let filterService;
let searchService;
let router;
const page = observableOf(0);
const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 });
const paginationService = new PaginationServiceStub(pagination); const paginationService = new PaginationServiceStub(pagination);
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ searchConfigurationService = new SearchConfigurationServiceStub();
searchFilterService = new SearchFilterServiceStub();
searchService = new SearchServiceStub(searchLink);
router = new RouterStub();
void TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
declarations: [SearchFacetSelectedOptionComponent], declarations: [SearchFacetSelectedOptionComponent],
providers: [ providers: [
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue:searchService },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: router },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ { provide: SearchConfigurationService, useValue: searchConfigurationService },
provide: SearchConfigurationService, useValue: { { provide: SearchFilterService, useValue: searchFilterService },
searchOptions: observableOf({})
}
},
{
provide: SearchFilterService, useValue: {
getSelectedValuesForFilter: () => selectedValues,
isFilterActiveWithValue: (paramName: string, filterValue: string) => observableOf(true),
getPage: (paramName: string) => page,
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
incrementPage: (filterName: string) => {
},
resetPage: (filterName: string) => {
}
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}
}
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchFacetSelectedOptionComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SearchFacetSelectedOptionComponent); fixture = TestBed.createComponent(SearchFacetSelectedOptionComponent);
comp = fixture.componentInstance; // SearchFacetSelectedOptionComponent test instance comp = fixture.componentInstance; // SearchFacetSelectedOptionComponent test instance
filterService = (comp as any).filterService; comp.selectedValue = appliedFilter;
searchService = (comp as any).searchService;
router = (comp as any).router;
comp.selectedValue = facetValue;
comp.selectedValues$ = selectedValues$;
comp.filterConfig = mockFilterConfig; comp.filterConfig = mockFilterConfig;
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('when the updateRemoveParams method is called wih a value', () => { describe('updateRemoveParams', () => {
it('should update the removeQueryParams with the new parameter values', () => { it('should always reset the page to 1', (done: DoneFn) => {
comp.removeQueryParams = {}; spyOn(searchConfigurationService, 'unselectAppliedFilterParams').and.returnValue(observableOf({
(comp as any).updateRemoveParams(selectedValues); [mockFilterConfig.paramName]: [`${value1},equals`],
expect(comp.removeQueryParams).toEqual({ ['page-id.page']: 5,
}));
comp.updateRemoveParams().pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual({
[mockFilterConfig.paramName]: [`${value1},equals`], [mockFilterConfig.paramName]: [`${value1},equals`],
['page-id.page']: 1 ['page-id.page']: 1
}); });
}); done();
});
describe('when filter type is authority and the updateRemoveParams method is called with a value', () => {
it('should update the removeQueryParams with the new parameter values', () => {
spyOn(filterService, 'getSelectedValuesForFilter').and.returnValue(selectedAuthorityValues);
comp.selectedValue = authorityValue;
comp.selectedValues$ = selectedAuthorityValues$;
comp.filterConfig = mockAuthorityFilterConfig;
comp.removeQueryParams = {};
fixture.detectChanges();
(comp as any).updateRemoveParams(selectedAuthorityValues);
expect(comp.removeQueryParams).toEqual({
[mockAuthorityFilterConfig.paramName]: [`${value1},${operator}`],
['page-id.page']: 1
}); });
}); });
}); });

View File

@@ -1,14 +1,12 @@
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { Observable } from 'rxjs';
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { SearchFilterConfig } from '../../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../../models/search-filter-config.model';
import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../../core/shared/search/search.service';
import { SearchFilterService } from '../../../../../../core/shared/search/search-filter.service';
import { hasValue } from '../../../../../empty.util';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { FacetValue } from '../../../../models/facet-value.model';
import { currentPath } from '../../../../../utils/route.utils'; import { currentPath } from '../../../../../utils/route.utils';
import { getFacetValueForType } from '../../../../search.utils'; import { AppliedFilter } from '../../../../models/applied-filter.model';
import { map } from 'rxjs/operators';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
@Component({ @Component({
@@ -20,47 +18,37 @@ import { PaginationService } from '../../../../../../core/pagination/pagination.
/** /**
* Represents a single selected option in a filter facet * Represents a single selected option in a filter facet
*/ */
export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { export class SearchFacetSelectedOptionComponent implements OnInit {
/** /**
* The value for this component * The value for this component
*/ */
@Input() selectedValue: FacetValue; @Input() selectedValue: AppliedFilter;
/** /**
* The filter configuration for this facet option * The filter configuration for this facet option
*/ */
@Input() filterConfig: SearchFilterConfig; @Input() filterConfig: SearchFilterConfig;
/**
* Emits the active values for this filter
*/
@Input() selectedValues$: Observable<FacetValue[]>;
/** /**
* True when the search component should show results on the current page * True when the search component should show results on the current page
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch: boolean;
/** /**
* UI parameters when this filter is removed * UI parameters when this filter is removed
*/ */
removeQueryParams; removeQueryParams: Observable<Params>;
/**
* Subscription to unsubscribe from on destroy
*/
sub: Subscription;
/** /**
* Link to the search page * Link to the search page
*/ */
searchLink: string; searchLink: string;
constructor(protected searchService: SearchService, constructor(
protected filterService: SearchFilterService, protected paginationService: PaginationService,
protected searchConfigService: SearchConfigurationService,
protected router: Router, protected router: Router,
protected paginationService: PaginationService protected searchService: SearchService,
protected searchConfigService: SearchConfigurationService,
) { ) {
} }
@@ -68,50 +56,31 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
* Initializes all observable instance variables and starts listening to them * Initializes all observable instance variables and starts listening to them
*/ */
ngOnInit(): void { ngOnInit(): void {
this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions)
.subscribe(([selectedValues, searchOptions]) => {
this.updateRemoveParams(selectedValues);
});
this.searchLink = this.getSearchLink(); this.searchLink = this.getSearchLink();
this.removeQueryParams = this.updateRemoveParams();
}
/**
* Calculates the parameters that should change if this {@link selectedValue} would be removed from the active filters
*/
updateRemoveParams(): Observable<Params> {
const page: string = this.paginationService.getPageParam(this.searchConfigService.paginationID);
return this.searchConfigService.unselectAppliedFilterParams(this.selectedValue.filter, this.selectedValue.value, this.selectedValue.operator).pipe(
map((params: Params) => ({
...params,
[page]: 1,
})),
);
} }
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
private getSearchLink(): string { getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return currentPath(this.router); return currentPath(this.router);
} }
return this.searchService.getSearchLink(); return this.searchService.getSearchLink();
} }
/**
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
* @param {string[]} selectedValues The values that are currently selected for this filter
*/
private updateRemoveParams(selectedValues: FacetValue[]): void {
const page = this.paginationService.getPageParam(this.searchConfigService.paginationID);
this.removeQueryParams = {
[this.filterConfig.paramName]: selectedValues
.filter((facetValue: FacetValue) => facetValue.label !== this.selectedValue.label)
.map((facetValue: FacetValue) => this.getFacetValue(facetValue)),
[page]: 1
};
}
/**
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
* Retrieve facet value related to facet type
*/
private getFacetValue(facetValue: FacetValue): string {
return getFacetValueForType(facetValue, this.filterConfig);
}
/**
* Make sure the subscription is unsubscribed from when this component is destroyed
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
} }

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="filters py-2"> <div class="filters py-2">
<ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option> <ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
<ng-container *ngFor="let page of (facetValues$ | async)"> <ng-container *ngFor="let page of (facetValues$ | async)">
<div [@facetLoad]="animationState"> <div [@facetLoad]="animationState">
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option> <ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
</div> </div>
</ng-container> </ng-container>
<div class="clearfix toggle-more-filters"> <div class="clearfix toggle-more-filters">

View File

@@ -1,9 +1,9 @@
<div> <div>
<div class="filters py-2"> <div class="filters py-2">
<ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option> <ds-search-facet-selected-option *ngFor="let value of (selectedAppliedFilters$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
<ng-container *ngFor="let page of (facetValues$ | async)"> <ng-container *ngFor="let page of (facetValues$ | async)">
<div [@facetLoad]="animationState"> <div [@facetLoad]="animationState">
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedAppliedFilters$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option> <ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
</div> </div>
</ng-container> </ng-container>
<div class="clearfix toggle-more-filters"> <div class="clearfix toggle-more-filters">

View File

@@ -1,13 +1,13 @@
<a *ngIf="min !== '*'" <a *ngIf="min !== '*'"
[routerLink]="searchLink" [routerLink]="searchLink"
[queryParams]="(removeParametersMin | async)" [queryParams]="(removeParametersMin$ | async)"
class="badge badge-primary mr-1 mb-1 text-capitalize"> class="badge badge-primary mr-1 mb-1 text-capitalize">
{{('search.filters.applied.f.' + appliedFilter.filter + '.min') | translate}}: {{ min }} {{('search.filters.applied.f.' + appliedFilter.filter + '.min') | translate}}: {{ min }}
<span> ×</span> <span> ×</span>
</a> </a>
<a *ngIf="max !== '*'" <a *ngIf="max !== '*'"
[routerLink]="searchLink" [routerLink]="searchLink"
[queryParams]="(removeParametersMax | async)" [queryParams]="(removeParametersMax$ | async)"
class="badge badge-primary mr-1 mb-1 text-capitalize"> class="badge badge-primary mr-1 mb-1 text-capitalize">
{{('search.filters.applied.f.' + appliedFilter.filter + '.max') | translate}}: {{ max }} {{('search.filters.applied.f.' + appliedFilter.filter + '.max') | translate}}: {{ max }}
<span> ×</span> <span> ×</span>

View File

@@ -10,17 +10,24 @@ import { addOperatorToFilterValue } from '../../search.utils';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../testing/pagination-service.stub';
import { take } from 'rxjs/operators';
import { of as observableOf } from 'rxjs';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
describe('SearchLabelComponent', () => { describe('SearchLabelRangeComponent', () => {
let comp: SearchLabelRangeComponent; let comp: SearchLabelRangeComponent;
let fixture: ComponentFixture<SearchLabelRangeComponent>; let fixture: ComponentFixture<SearchLabelRangeComponent>;
let route: ActivatedRouteStub; let route: ActivatedRouteStub;
let searchConfigurationService: SearchConfigurationServiceStub; let searchConfigurationService: SearchConfigurationServiceStub;
let paginationService: PaginationServiceStub;
const searchLink = '/search'; const searchLink = '/search';
let appliedFilter: AppliedFilter; let appliedFilter: AppliedFilter;
let initialRouteParams: Params; let initialRouteParams: Params;
let pagination: PaginationComponentOptions;
function init(): void { function init(): void {
appliedFilter = Object.assign(new AppliedFilter(), { appliedFilter = Object.assign(new AppliedFilter(), {
@@ -31,16 +38,22 @@ describe('SearchLabelComponent', () => {
}); });
initialRouteParams = { initialRouteParams = {
'query': '', 'query': '',
'spc.page': '1', 'page-id.page': '5',
'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator), 'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator),
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'), 'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
}; };
pagination = Object.assign(new PaginationComponentOptions(), {
id: 'page-id',
currentPage: 1,
pageSize: 20,
});
} }
beforeEach(waitForAsync(async () => { beforeEach(waitForAsync(async () => {
init(); init();
route = new ActivatedRouteStub(initialRouteParams); route = new ActivatedRouteStub(initialRouteParams);
searchConfigurationService = new SearchConfigurationServiceStub(); searchConfigurationService = new SearchConfigurationServiceStub();
paginationService = new PaginationServiceStub(pagination);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
@@ -51,6 +64,7 @@ describe('SearchLabelComponent', () => {
SearchLabelRangeComponent, SearchLabelRangeComponent,
], ],
providers: [ providers: [
{ provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigurationService }, { provide: SearchConfigurationService, useValue: searchConfigurationService },
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
@@ -65,7 +79,16 @@ describe('SearchLabelComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { describe('updateRemoveParams', () => {
expect(comp).toBeTruthy(); it('should always reset the page to 1', (done: DoneFn) => {
spyOn(searchConfigurationService, 'unselectAppliedFilterParams').and.returnValue(observableOf(initialRouteParams));
comp.updateRemoveParams('f.dateIssued.max', '2000').pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual(Object.assign({}, initialRouteParams, {
'page-id.page': 1,
}));
done();
});
});
}); });
}); });

View File

@@ -6,6 +6,8 @@ import { currentPath } from '../../../utils/route.utils';
import { AppliedFilter } from '../../models/applied-filter.model'; import { AppliedFilter } from '../../models/applied-filter.model';
import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator'; import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { map } from 'rxjs/operators';
import { PaginationService } from '../../../../core/pagination/pagination.service';
/** /**
* Component that represents the label containing the currently active filters * Component that represents the label containing the currently active filters
@@ -23,18 +25,19 @@ export class SearchLabelRangeComponent implements OnInit {
searchLink: string; searchLink: string;
removeParametersMin: Observable<Params>; removeParametersMin$: Observable<Params>;
removeParametersMax: Observable<Params>; removeParametersMax$: Observable<Params>;
min: string; min: string;
max: string; max: string;
constructor( constructor(
protected paginationService: PaginationService,
protected router: Router,
protected searchConfigurationService: SearchConfigurationService, protected searchConfigurationService: SearchConfigurationService,
protected searchService: SearchService, protected searchService: SearchService,
protected router: Router,
) { ) {
} }
@@ -42,14 +45,31 @@ export class SearchLabelRangeComponent implements OnInit {
this.searchLink = this.getSearchLink(); this.searchLink = this.getSearchLink();
this.min = this.appliedFilter.value.substring(1, this.appliedFilter.value.indexOf('TO') - 1); this.min = this.appliedFilter.value.substring(1, this.appliedFilter.value.indexOf('TO') - 1);
this.max = this.appliedFilter.value.substring(this.appliedFilter.value.indexOf('TO') + 3, this.appliedFilter.value.length - 1); this.max = this.appliedFilter.value.substring(this.appliedFilter.value.indexOf('TO') + 3, this.appliedFilter.value.length - 1);
this.removeParametersMin = this.searchConfigurationService.getParamsWithoutAppliedFilter(`${this.appliedFilter.filter}.min`, this.min); this.removeParametersMin$ = this.updateRemoveParams(`${this.appliedFilter.filter}.min`, this.min);
this.removeParametersMax = this.searchConfigurationService.getParamsWithoutAppliedFilter(`${this.appliedFilter.filter}.max`, this.max); this.removeParametersMax$ = this.updateRemoveParams(`${this.appliedFilter.filter}.max`, this.max);
}
/**
* Calculates the parameters that should change if this {@link appliedFilter} would be removed from the active filters
*
* @param filterName The {@link AppliedFilter}'s name
* @param value The {@link AppliedFilter}'s value
* @param operator The {@link AppliedFilter}'s optional operator
*/
updateRemoveParams(filterName: string, value: string, operator?: string): Observable<Params> {
const page: string = this.paginationService.getPageParam(this.searchConfigurationService.paginationID);
return this.searchConfigurationService.unselectAppliedFilterParams(filterName, value, operator).pipe(
map((params: Params) => ({
...params,
[page]: 1,
})),
);
} }
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
private getSearchLink(): string { getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return currentPath(this.router); return currentPath(this.router);
} }

View File

@@ -1,6 +1,6 @@
<a class="badge badge-primary mr-1 mb-1 text-capitalize" <a class="badge badge-primary mr-1 mb-1 text-capitalize"
[routerLink]="searchLink" [routerLink]="searchLink"
[queryParams]="(removeParameters | async)"> [queryParams]="(removeParameters$ | async)">
{{('search.filters.applied.f.' + appliedFilter.filter) | translate}}: {{'search.filters.' + appliedFilter.filter + '.' + appliedFilter.label | translate: {default: appliedFilter.label} }} {{('search.filters.applied.f.' + appliedFilter.filter) | translate}}: {{'search.filters.' + appliedFilter.filter + '.' + appliedFilter.label | translate: {default: appliedFilter.label} }}
<span> ×</span> <span> ×</span>
</a> </a>

View File

@@ -10,6 +10,11 @@ import { addOperatorToFilterValue } from '../../search.utils';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../testing/pagination-service.stub';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
import { of as observableOf } from 'rxjs';
import { take } from 'rxjs/operators';
describe('SearchLabelComponent', () => { describe('SearchLabelComponent', () => {
let comp: SearchLabelComponent; let comp: SearchLabelComponent;
@@ -17,10 +22,12 @@ describe('SearchLabelComponent', () => {
let route: ActivatedRouteStub; let route: ActivatedRouteStub;
let searchConfigurationService: SearchConfigurationServiceStub; let searchConfigurationService: SearchConfigurationServiceStub;
let paginationService: PaginationServiceStub;
const searchLink = '/search'; const searchLink = '/search';
let appliedFilter: AppliedFilter; let appliedFilter: AppliedFilter;
let initialRouteParams: Params; let initialRouteParams: Params;
let pagination: PaginationComponentOptions;
function init(): void { function init(): void {
appliedFilter = Object.assign(new AppliedFilter(), { appliedFilter = Object.assign(new AppliedFilter(), {
@@ -35,12 +42,18 @@ describe('SearchLabelComponent', () => {
'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator), 'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator),
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'), 'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
}; };
pagination = Object.assign(new PaginationComponentOptions(), {
id: 'page-id',
currentPage: 1,
pageSize: 20,
});
} }
beforeEach(waitForAsync(async () => { beforeEach(waitForAsync(async () => {
init(); init();
route = new ActivatedRouteStub(initialRouteParams); route = new ActivatedRouteStub(initialRouteParams);
searchConfigurationService = new SearchConfigurationServiceStub(); searchConfigurationService = new SearchConfigurationServiceStub();
paginationService = new PaginationServiceStub(pagination);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
@@ -51,6 +64,7 @@ describe('SearchLabelComponent', () => {
SearchLabelComponent, SearchLabelComponent,
], ],
providers: [ providers: [
{ provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigurationService }, { provide: SearchConfigurationService, useValue: searchConfigurationService },
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
@@ -65,7 +79,16 @@ describe('SearchLabelComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { describe('updateRemoveParams', () => {
expect(comp).toBeTruthy(); it('should always reset the page to 1', (done: DoneFn) => {
spyOn(searchConfigurationService, 'unselectAppliedFilterParams').and.returnValue(observableOf(initialRouteParams));
comp.updateRemoveParams().pipe(take(1)).subscribe((params: Params) => {
expect(params).toEqual(Object.assign({}, initialRouteParams, {
'page-id.page': 1,
}));
done();
});
});
}); });
}); });

View File

@@ -6,6 +6,8 @@ import { currentPath } from '../../../utils/route.utils';
import { AppliedFilter } from '../../models/applied-filter.model'; import { AppliedFilter } from '../../models/applied-filter.model';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator'; import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator';
import { map } from 'rxjs/operators';
import { PaginationService } from '../../../../core/pagination/pagination.service';
/** /**
* Component that represents the label containing the currently active filters * Component that represents the label containing the currently active filters
@@ -25,27 +27,41 @@ export class SearchLabelComponent implements OnInit {
@Input() inPlaceSearch: boolean; @Input() inPlaceSearch: boolean;
@Input() appliedFilter: AppliedFilter; @Input() appliedFilter: AppliedFilter;
searchLink: string; searchLink: string;
removeParameters: Observable<Params>; removeParameters$: Observable<Params>;
/** /**
* Initialize the instance variable * Initialize the instance variable
*/ */
constructor( constructor(
protected paginationService: PaginationService,
protected router: Router,
protected searchConfigurationService: SearchConfigurationService, protected searchConfigurationService: SearchConfigurationService,
protected searchService: SearchService, protected searchService: SearchService,
protected router: Router,
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.searchLink = this.getSearchLink(); this.searchLink = this.getSearchLink();
this.removeParameters = this.searchConfigurationService.getParamsWithoutAppliedFilter(this.appliedFilter.filter, this.appliedFilter.value, this.appliedFilter.operator); this.removeParameters$ = this.updateRemoveParams();
}
/**
* Calculates the parameters that should change if this {@link appliedFilter} would be removed from the active filters
*/
updateRemoveParams(): Observable<Params> {
const page: string = this.paginationService.getPageParam(this.searchConfigurationService.paginationID);
return this.searchConfigurationService.unselectAppliedFilterParams(this.appliedFilter.filter, this.appliedFilter.value, this.appliedFilter.operator).pipe(
map((params: Params) => ({
...params,
[page]: 1,
})),
);
} }
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
private getSearchLink(): string { getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return currentPath(this.router); return currentPath(this.router);
} }

View File

@@ -32,7 +32,11 @@ export class SearchConfigurationServiceStub {
return observableOf([{value: 'test', label: 'test'}]); return observableOf([{value: 'test', label: 'test'}]);
} }
getParamsWithoutAppliedFilter(_filterName: string, _value: string, _operator?: string): Observable<Params> { unselectAppliedFilterParams(_filterName: string, _value: string, _operator?: string): Observable<Params> {
return observableOf({});
}
selectNewAppliedFilterParams(_filterName: string, _value: string, _operator?: string): Observable<Params> {
return observableOf({}); return observableOf({});
} }

View File

@@ -0,0 +1,75 @@
import { Observable, of as observableOf } from 'rxjs';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
import { SearchFilterConfig } from '../search/models/search-filter-config.model';
import { Params } from '@angular/router';
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
export class SearchFilterServiceStub {
isFilterActiveWithValue(_paramName: string, _filterValue: string): Observable<boolean> {
return observableOf(true);
}
isFilterActive(_paramName: string): Observable<boolean> {
return observableOf(true);
}
getCurrentScope(): Observable<string> {
return observableOf(undefined);
}
getCurrentQuery(): Observable<string> {
return observableOf(undefined);
}
getCurrentPagination(_pagination: any = {}): Observable<PaginationComponentOptions> {
return Object.assign(new PaginationComponentOptions());
}
getCurrentSort(_defaultSort: SortOptions): Observable<SortOptions> {
return observableOf(new SortOptions('', SortDirection.ASC));
}
getCurrentFilters(): Observable<Params> {
return observableOf({});
}
getCurrentView(): Observable<string> {
return observableOf(undefined);
}
getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable<string[]> {
return observableOf([]);
}
isCollapsed(_filterName: string): Observable<boolean> {
return observableOf(true);
}
getPage(_filterName: string): Observable<number> {
return observableOf(1);
}
collapse(_filterName: string): void {
}
expand(_filterName: string): void {
}
toggle(_filterName: string): void {
}
initializeFilter(_filter: SearchFilterConfig): void {
}
decrementPage(_filterName: string): void {
}
incrementPage(_filterName: string): void {
}
resetPage(_filterName: string): void {
}
}