diff --git a/src/app/core/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts index 18a916753af..93525ac8183 100644 --- a/src/app/core/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -9,6 +9,8 @@ import { RouteService } from './route.service'; import { RouterMock } from '../../shared/mocks/router.mock'; import { TestScheduler } from 'rxjs/testing'; import { AddUrlToHistoryAction } from '../history/history.actions'; +import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub'; +import { take } from 'rxjs/operators'; describe('RouteService', () => { let scheduler: TestScheduler; @@ -29,6 +31,7 @@ describe('RouteService', () => { select: jasmine.createSpy('select') }); + let route: ActivatedRouteStub; const router = new RouterMock(); router.setParams(convertToParamMap(paramObject)); @@ -36,16 +39,11 @@ describe('RouteService', () => { paramObject[paramName2] = [paramValue2a, paramValue2b]; beforeEach(waitForAsync(() => { + route = new ActivatedRouteStub(paramObject); + return TestBed.configureTestingModule({ providers: [ - { - provide: ActivatedRoute, - useValue: { - queryParams: observableOf(paramObject), - params: observableOf(paramObject), - queryParamMap: observableOf(convertToParamMap(paramObject)) - }, - }, + { provide: ActivatedRoute, useValue: route }, { provide: Router, useValue: router }, { provide: Store, useValue: store }, ] @@ -181,4 +179,51 @@ 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 remove the parameter completely if only one value is defined in an array', (done: DoneFn) => { + route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,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.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(); + }); + }); + }); }); diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index 53817625709..35a6ad962d4 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -225,4 +225,56 @@ 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 { + 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); + if (newParams[name].length === 0) { + delete newParams[name]; + } + } + 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 { + 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; + }), + ); + } + } diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index d0fbd46d3a2..c13ada13b4e 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -13,6 +13,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { RequestEntry } from '../../data/request-entry.model'; import { SearchObjects } from '../../../shared/search/models/search-objects.model'; +import { AppliedFilter } from '../../../shared/search/models/applied-filter.model'; describe('SearchConfigurationService', () => { let service: SearchConfigurationService; @@ -38,13 +39,14 @@ describe('SearchConfigurationService', () => { const routeService = jasmine.createSpyObj('RouteService', { getQueryParameterValue: observableOf(value1), getQueryParamsWithPrefix: observableOf(prefixFilter), - getRouteParameterValue: observableOf('') + getRouteParameterValue: observableOf(''), + getParamsExceptValue: observableOf({}), }); const paginationService = new PaginationServiceStub(); - const activatedRoute: any = new ActivatedRouteStub(); + const activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); const linkService: any = {}; const requestService: any = getMockRequestService(); const halService: any = { @@ -70,7 +72,7 @@ describe('SearchConfigurationService', () => { } }; beforeEach(() => { - service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute, linkService, halService, requestService, rdb); + service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute as any, linkService, halService, requestService, rdb); }); describe('when the scope is called', () => { @@ -279,4 +281,29 @@ describe('SearchConfigurationService', () => { expect((service as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true); }); }); + + describe('unselectAppliedFilterParams', () => { + let appliedFilter: AppliedFilter; + + beforeEach(() => { + appliedFilter = Object.assign(new AppliedFilter(), { + filter: 'author', + operator: 'authority', + value: '1282121b-5394-4689-ab93-78d537764052', + label: 'Odinson, Thor', + }); + }); + + it('should return all params except the applied filter', () => { + service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator); + + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority'); + }); + + it('should be able to remove AppliedFilter without operator', () => { + service.unselectAppliedFilterParams('dateIssued.max', '2000'); + + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000'); + }); + }); }); diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index eed93ae201c..eb88c9e7345 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -28,6 +28,7 @@ import { FacetConfigResponseParsingService } from '../../data/facet-config-respo import { ViewMode } from '../view-mode.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; import { FacetConfigResponse } from '../../../shared/search/models/facet-config-response.model'; +import { addOperatorToFilterValue } from '../../../shared/search/search.utils'; /** * Service that performs all actions that have to do with the current search configuration @@ -525,6 +526,27 @@ export class SearchConfigurationService implements OnDestroy { ); } + /** + * 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 + */ + unselectAppliedFilterParams(filterName: string, value: string, operator?: string): Observable { + return this.routeService.getParamsExceptValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value); + } + + /** + * 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 { + return this.routeService.getParamsWithAdditionalValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value); + } /** * @returns {Observable} Emits the current view mode as a partial SearchOptions object diff --git a/src/app/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index 80ba200d383..2eafdf268af 100644 --- a/src/app/core/shared/search/search-filter.service.ts +++ b/src/app/core/shared/search/search-filter.service.ts @@ -1,6 +1,6 @@ import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; -import { Injectable, InjectionToken } from '@angular/core'; +import { Injectable, InjectionToken, EventEmitter } from '@angular/core'; import { SearchFiltersState, SearchFilterState @@ -21,12 +21,14 @@ import { SortDirection, SortOptions } from '../../cache/models/sort-options.mode import { RouteService } from '../../services/route.service'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { Params } from '@angular/router'; +import { AppliedFilter } from '../../../shared/search/models/applied-filter.model'; const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; export const FILTER_CONFIG: InjectionToken = new InjectionToken('filterConfig'); export const IN_PLACE_SEARCH: InjectionToken = new InjectionToken('inPlaceSearch'); export const REFRESH_FILTER: InjectionToken> = new InjectionToken('refreshFilters'); +export const CHANGE_APPLIED_FILTERS: InjectionToken> = new InjectionToken('changeAppliedFilters'); /** * Service that performs all actions that have to do with search filters and facets @@ -61,7 +63,7 @@ export class SearchFilterService { * Fetch the current active scope from the query parameters * @returns {Observable} */ - getCurrentScope() { + getCurrentScope(): Observable { return this.routeService.getQueryParameterValue('scope'); } @@ -69,7 +71,7 @@ export class SearchFilterService { * Fetch the current query from the query parameters * @returns {Observable} */ - getCurrentQuery() { + getCurrentQuery(): Observable { return this.routeService.getQueryParameterValue('query'); } @@ -111,7 +113,7 @@ export class SearchFilterService { * Fetch the current active filters from the query parameters * @returns {Observable} */ - getCurrentFilters() { + getCurrentFilters(): Observable { return this.routeService.getQueryParamsWithPrefix('f.'); } @@ -119,7 +121,7 @@ export class SearchFilterService { * Fetch the current view from the query parameters * @returns {Observable} */ - getCurrentView() { + getCurrentView(): Observable { return this.routeService.getQueryParameterValue('view'); } diff --git a/src/app/shared/search/models/applied-filter.model.ts b/src/app/shared/search/models/applied-filter.model.ts new file mode 100644 index 00000000000..279e1c32012 --- /dev/null +++ b/src/app/shared/search/models/applied-filter.model.ts @@ -0,0 +1,17 @@ +import { autoserialize } from 'cerialize'; + +export class AppliedFilter { + + @autoserialize + filter: string; + + @autoserialize + operator: string; + + @autoserialize + value: string; + + @autoserialize + label: string; + +} diff --git a/src/app/shared/search/models/search-query-response.model.ts b/src/app/shared/search/models/search-query-response.model.ts index a436a7c0454..31264d35573 100644 --- a/src/app/shared/search/models/search-query-response.model.ts +++ b/src/app/shared/search/models/search-query-response.model.ts @@ -1,6 +1,8 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, autoserializeAs } from 'cerialize'; import { PageInfo } from '../../../core/shared/page-info.model'; import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { AppliedFilter } from './applied-filter.model'; +import { SearchResultSorting } from './search-result-sorting.model'; /** * Class representing the response returned by the server when performing a search request @@ -21,14 +23,14 @@ export abstract class SearchQueryResponse extends PaginatedList { /** * The currently active filters used in the search request */ - @autoserialize - appliedFilters: any[]; // TODO + @autoserializeAs(AppliedFilter) + appliedFilters: AppliedFilter[]; /** * The sort parameters used in the search request */ - @autoserialize - sort: any; // TODO + @autoserializeAs(SearchResultSorting) + sort: SearchResultSorting; /** * The sort parameters used in the search request diff --git a/src/app/shared/search/models/search-result-sorting.model.ts b/src/app/shared/search/models/search-result-sorting.model.ts new file mode 100644 index 00000000000..e3560eefd40 --- /dev/null +++ b/src/app/shared/search/models/search-result-sorting.model.ts @@ -0,0 +1,11 @@ +import { autoserialize } from 'cerialize'; + +export class SearchResultSorting { + + @autoserialize + by: string; + + @autoserialize + order: string; + +} diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index fdf154bc045..e008226d19a 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -1,9 +1,9 @@
- - + +
- +
diff --git a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html index 7d0ad899142..07fa57b0cce 100644 --- a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html @@ -1,9 +1,9 @@
- - + +
- +
diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html index 9a4bffadb8d..817b1e93168 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html @@ -1,7 +1,7 @@ + [queryParams]="addQueryParams$ | async"> + [queryParams]="removeQueryParams | async">
diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts index d1d3bd729d4..f41c61342c9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit, Output, EventEmitter } from '@angular/core'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; @@ -11,6 +11,7 @@ import { SearchService } from '../../../../core/shared/search/search.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; import { SequenceService } from '../../../../core/shared/sequence.service'; +import { AppliedFilter } from '../../models/applied-filter.model'; @Component({ selector: 'ds-search-filter', @@ -38,6 +39,11 @@ export class SearchFilterComponent implements OnInit { */ @Input() refreshFilters: BehaviorSubject; + /** + * Emits the {@link AppliedFilter}s of this search filter + */ + @Output() changeAppliedFilters: EventEmitter = new EventEmitter(); + /** * True when the filter is 100% collapsed in the UI */ diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index eb49235641f..1875cc8076a 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -1,9 +1,9 @@
- - + +
- +
diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index e6c74d80478..949fc33892e 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -13,6 +13,7 @@ import { PageInfo } from '../../../../../core/shared/page-info.model'; import { CommonModule } from '@angular/common'; import { SearchService } from '../../../../../core/shared/search/search.service'; import { + CHANGE_APPLIED_FILTERS, FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService, @@ -24,7 +25,7 @@ import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub'; import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; -import { FacetValue} from '../../../models/facet-value.model'; +import { AppliedFilter } from '../../../models/applied-filter.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; describe('SearchHierarchyFilterComponent', () => { @@ -75,7 +76,8 @@ describe('SearchHierarchyFilterComponent', () => { { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, - { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false)} + { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) }, + { provide: CHANGE_APPLIED_FILTERS, useValue: new EventEmitter() }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -124,8 +126,8 @@ describe('SearchHierarchyFilterComponent', () => { beforeEach(async () => { showVocabularyTreeLink.nativeElement.click(); - fixture.componentInstance.selectedValues$ = observableOf( - alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value })) + fixture.componentInstance.selectedAppliedFilters$ = observableOf( + alreadySelectedValues.map(value => Object.assign(new AppliedFilter(), { value })) ); VocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), { value: newSelectedValue, diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index f9b3f2bff94..649624bcb4b 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, EventEmitter } from '@angular/core'; import { renderFacetFor } from '../search-filter-type-decorator'; import { FilterType } from '../../../models/filter-type.model'; import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; @@ -10,15 +10,13 @@ import { SearchService } from '../../../../../core/shared/search/search.service' import { FILTER_CONFIG, IN_PLACE_SEARCH, - SearchFilterService, REFRESH_FILTER + SearchFilterService, REFRESH_FILTER, CHANGE_APPLIED_FILTERS } from '../../../../../core/shared/search/search-filter.service'; -import { Router } from '@angular/router'; +import { Params, Router } from '@angular/router'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; -import { FacetValue } from '../../../models/facet-value.model'; -import { getFacetValueForType } from '../../../search.utils'; import { filter, map, take } from 'rxjs/operators'; import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; import { Observable, BehaviorSubject } from 'rxjs'; @@ -26,6 +24,7 @@ import { PageInfo } from '../../../../../core/shared/page-info.model'; import { environment } from '../../../../../../environments/environment'; import { addOperatorToFilterValue } from '../../../search.utils'; import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component'; +import { AppliedFilter } from '../../../models/applied-filter.model'; @Component({ selector: 'ds-search-hierarchy-filter', @@ -49,9 +48,10 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, - @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject + @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject, + @Inject(CHANGE_APPLIED_FILTERS) public changeAppliedFilters: EventEmitter, ) { - super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, changeAppliedFilters); } vocabularyExists$: Observable; @@ -92,20 +92,14 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i closed: true }; modalRef.result.then((detail: VocabularyEntryDetail) => { - this.selectedValues$ - .pipe(take(1)) - .subscribe((selectedValues) => { - this.router.navigate( - [this.searchService.getSearchLink()], - { - queryParams: { - [this.filterConfig.paramName]: [...selectedValues, {value: detail.value}] - .map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), - }, - queryParamsHandling: 'merge', - }, - ); - }); + this.subs.push(this.searchConfigService.selectNewAppliedFilterParams(this.filterConfig.name, detail.value, 'equals').pipe(take(1)).subscribe((params: Params) => { + void this.router.navigate( + [this.searchService.getSearchLink()], + { + queryParams: params, + }, + ); + })); }).catch(); } diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html index 7834c4c5571..42c434f3bf9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html @@ -39,7 +39,7 @@ [(ngModel)]="range" ngDefaultControl> - +
diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 3a146f50590..509ef48bf86 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -6,7 +6,7 @@ import { FILTER_CONFIG, IN_PLACE_SEARCH, REFRESH_FILTER, - SearchFilterService + SearchFilterService, CHANGE_APPLIED_FILTERS } from '../../../../../core/shared/search/search-filter.service'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { FilterType } from '../../../models/filter-type.model'; @@ -105,6 +105,7 @@ describe('SearchRangeFilterComponent', () => { { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) }, + { provide: CHANGE_APPLIED_FILTERS, useValue: new EventEmitter() }, { provide: SearchFilterService, useValue: { getSelectedValuesForFilter: () => selectedValues, diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index 938f67412e4..48c039c538f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -1,13 +1,14 @@ -import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, of as observableOf , Subscription } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { isPlatformBrowser } from '@angular/common'; -import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID, EventEmitter } from '@angular/core'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { FilterType } from '../../../models/filter-type.model'; import { renderFacetFor } from '../search-filter-type-decorator'; import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { + CHANGE_APPLIED_FILTERS, FILTER_CONFIG, IN_PLACE_SEARCH, REFRESH_FILTER, @@ -19,6 +20,8 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-p import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { RouteService } from '../../../../../core/services/route.service'; import { hasValue } from '../../../../empty.util'; +import { AppliedFilter } from '../../../models/applied-filter.model'; +import { FacetValues } from '../../../models/facet-values.model'; import { yearFromString } from 'src/app/shared/date.util'; /** @@ -61,7 +64,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple /** * The current range of the filter */ - range; + range: [number | undefined, number | undefined]; /** * Subscription to unsubscribe from @@ -83,8 +86,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @Inject(PLATFORM_ID) private platformId: any, @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject, + @Inject(CHANGE_APPLIED_FILTERS) public changeAppliedFilters: EventEmitter, private route: RouteService) { - super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, changeAppliedFilters); } @@ -98,13 +102,26 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple this.max = yearFromString(this.filterConfig.maxValue) || this.max; const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)); const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)); - this.sub = observableCombineLatest(iniMin, iniMax).pipe( - map(([min, max]) => { - const minimum = hasValue(min) ? min : this.min; - const maximum = hasValue(max) ? max : this.max; + this.sub = observableCombineLatest([iniMin, iniMax]).pipe( + map(([min, max]: [string, string]) => { + const minimum = hasValue(min) ? Number(min) : this.min; + const maximum = hasValue(max) ? Number(max) : this.max; return [minimum, maximum]; }) - ).subscribe((minmax) => this.range = minmax); + ).subscribe((minmax: [number, number]) => this.range = minmax); + } + + setAppliedFilter(allFacetValues: FacetValues[]): void { + const appliedFilters: AppliedFilter[] = [].concat(...allFacetValues.map((facetValues: FacetValues) => facetValues.appliedFilters)) + .filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter)) + .filter((appliedFilter: AppliedFilter) => appliedFilter.filter === this.filterConfig.name) + // TODO this should ideally be fixed in the backend + .map((appliedFilter: AppliedFilter) => Object.assign({}, appliedFilter, { + operator: 'range', + })); + + this.selectedAppliedFilters$ = observableOf(appliedFilters); + this.changeAppliedFilters.emit(appliedFilters); } /** @@ -117,7 +134,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple const newMin = this.range[0] !== this.min ? [this.range[0]] : null; const newMax = this.range[1] !== this.max ? [this.range[1]] : null; - this.router.navigate(this.getSearchLinkParts(), { + void this.router.navigate(this.getSearchLinkParts(), { queryParams: { [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin, diff --git a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html index fdf154bc045..784592d0be3 100644 --- a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html @@ -1,9 +1,9 @@
- - + +
- +
diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index e392cd2663e..820be2ebdf3 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -1,7 +1,7 @@

{{"search.filters.head" | translate}}

- +
{{"search.filters.reset" | translate}} diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index 766939226dd..5cf98db2df8 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnDestroy, OnInit, Output, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -8,10 +8,10 @@ import { SearchService } from '../../../core/shared/search/search.service'; import { RemoteData } from '../../../core/data/remote-data'; import { SearchFilterConfig } from '../models/search-filter-config.model'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; -import { SearchFilterService } from '../../../core/shared/search/search-filter.service'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { currentPath } from '../../utils/route.utils'; import { hasValue } from '../../empty.util'; +import { AppliedFilter } from '../models/applied-filter.model'; @Component({ selector: 'ds-search-filters', @@ -55,6 +55,13 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { */ @Input() refreshFilters: BehaviorSubject; + /** + * Emits the {@link AppliedFilter}s by search filter name + */ + @Output() changeAppliedFilters: EventEmitter> = new EventEmitter(); + + appliedFilters: Map = new Map(); + /** * Link to the search page */ @@ -71,7 +78,6 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { */ constructor( private searchService: SearchService, - private filterService: SearchFilterService, private router: Router, @Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) { } @@ -101,6 +107,17 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { return config ? config.name : undefined; } + /** + * Updates the map of {@link AppliedFilter}s and emits it to it's parent component + * + * @param filterName + * @param appliedFilters + */ + updateAppliedFilters(filterName: string, appliedFilters: AppliedFilter[]): void { + this.appliedFilters.set(filterName, appliedFilters); + this.changeAppliedFilters.emit(this.appliedFilters); + } + ngOnDestroy() { this.subs.forEach((sub) => { if (hasValue(sub)) { diff --git a/src/app/shared/search/search-labels/search-label-loader/search-label-loader-directive.directive.ts b/src/app/shared/search/search-labels/search-label-loader/search-label-loader-directive.directive.ts new file mode 100644 index 00000000000..43c9d833a7b --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-loader/search-label-loader-directive.directive.ts @@ -0,0 +1,16 @@ +import { Directive, ViewContainerRef } from '@angular/core'; + +/** + * Directive used as a hook to know where to inject the dynamic loaded component + */ +@Directive({ + selector: '[dsSearchLabelLoader]' +}) +export class SearchLabelLoaderDirective { + + constructor( + public viewContainerRef: ViewContainerRef, + ) { + } + +} diff --git a/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.html b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.html new file mode 100644 index 00000000000..f67716bc7d8 --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.ts b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.ts new file mode 100644 index 00000000000..763dc1b0323 --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.component.ts @@ -0,0 +1,132 @@ +import { Component, ComponentRef, OnChanges, OnDestroy, OnInit, ViewChild, ViewContainerRef, SimpleChanges, Input } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { GenericConstructor } from 'src/app/core/shared/generic-constructor'; +import { hasValue, isNotEmpty } from 'src/app/shared/empty.util'; +import { ThemeService } from '../../../theme-support/theme.service'; +import { SearchLabelLoaderDirective } from './search-label-loader-directive.directive'; +import { getSearchLabelByOperator } from './search-label-loader.decorator'; +import { AppliedFilter } from '../../models/applied-filter.model'; + +@Component({ + selector: 'ds-search-label-loader', + templateUrl: './search-label-loader.component.html', +}) +export class SearchLabelLoaderComponent implements OnInit, OnChanges, OnDestroy { + + @Input() inPlaceSearch: boolean; + + @Input() appliedFilter: AppliedFilter; + + /** + * Directive to determine where the dynamic child component is located + */ + @ViewChild(SearchLabelLoaderDirective, { static: true }) componentDirective: SearchLabelLoaderDirective; + + /** + * The reference to the dynamic component + */ + protected compRef: ComponentRef; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + */ + protected subs: Subscription[] = []; + + /** + * The @Input() that are used to find the matching component using {@link getComponent}. When the value of + * one of these @Input() change this loader needs to retrieve the best matching component again using the + * {@link getComponent} method. + */ + protected inputNamesDependentForComponent: (keyof this & string)[] = []; + + /** + * The list of the @Input() names that should be passed down to the dynamically created components. + */ + protected inputNames: (keyof this & string)[] = [ + 'inPlaceSearch', + 'appliedFilter', + ]; + + constructor( + protected themeService: ThemeService, + ) { + } + + /** + * Set up the dynamic child component + */ + ngOnInit(): void { + this.instantiateComponent(); + } + + /** + * Whenever the inputs change, update the inputs of the dynamic component + */ + ngOnChanges(changes: SimpleChanges): void { + if (hasValue(this.compRef)) { + if (this.inputNamesDependentForComponent.some((name: keyof this & string) => hasValue(changes[name]) && changes[name].previousValue !== changes[name].currentValue)) { + // Recreate the component when the @Input()s used by getComponent() aren't up-to-date anymore + this.destroyComponentInstance(); + this.instantiateComponent(); + } else { + this.connectInputsAndOutputs(); + } + } + } + + ngOnDestroy(): void { + this.subs + .filter((subscription: Subscription) => hasValue(subscription)) + .forEach((subscription: Subscription) => subscription.unsubscribe()); + this.destroyComponentInstance(); + } + + /** + * Creates the component and connects the @Input() & @Output() from the ThemedComponent to its child Component. + */ + public instantiateComponent(): void { + const component: GenericConstructor = this.getComponent(); + + const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef; + viewContainerRef.clear(); + + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + injector: undefined, + }, + ); + + this.connectInputsAndOutputs(); + } + + /** + * Destroys the themed component and calls it's `ngOnDestroy` + */ + public destroyComponentInstance(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = null; + } + } + + /** + * Fetch the component depending on the item's entity type, metadata representation type and context + */ + public getComponent(): GenericConstructor { + return getSearchLabelByOperator(this.appliedFilter.operator); + } + + /** + * Connect the inputs and outputs of this component to the dynamic component, + * to ensure they're in sync + */ + public connectInputsAndOutputs(): void { + if (isNotEmpty(this.inputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) { + this.inputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => { + this.compRef.instance[name] = this[name]; + }); + } + } + +} diff --git a/src/app/shared/search/search-labels/search-label-loader/search-label-loader.decorator.ts b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.decorator.ts new file mode 100644 index 00000000000..ffd73af6ba1 --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-loader/search-label-loader.decorator.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { GenericConstructor } from '../../../../core/shared/generic-constructor'; + +export const map: Map> = new Map(); + +export function renderSearchLabelFor(operator: string) { + return function decorator(objectElement: any) { + if (!objectElement) { + return; + } + map.set(operator, objectElement); + }; +} + +export function getSearchLabelByOperator(operator: string): GenericConstructor { + return map.get(operator); +} diff --git a/src/app/shared/search/search-labels/search-label-range/search-label-range.component.html b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.html new file mode 100644 index 00000000000..7174da1e955 --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.html @@ -0,0 +1,14 @@ + + {{('search.filters.applied.f.' + appliedFilter.filter + '.min') | translate}}: {{ min }} + × + + + {{('search.filters.applied.f.' + appliedFilter.filter + '.max') | translate}}: {{ max }} + × + diff --git a/src/app/shared/search/search-labels/search-label-range/search-label-range.component.spec.ts b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.spec.ts new file mode 100644 index 00000000000..658bf46e514 --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.spec.ts @@ -0,0 +1,94 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { Params, ActivatedRoute } from '@angular/router'; +import { SearchLabelRangeComponent } from './search-label-range.component'; +import { SearchServiceStub } from '../../../testing/search-service.stub'; +import { SearchService } from '../../../../core/shared/search/search.service'; +import { ActivatedRouteStub } from '../../../testing/active-router.stub'; +import { AppliedFilter } from '../../models/applied-filter.model'; +import { addOperatorToFilterValue } from '../../search.utils'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +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('SearchLabelRangeComponent', () => { + let comp: SearchLabelRangeComponent; + let fixture: ComponentFixture; + + let route: ActivatedRouteStub; + let searchConfigurationService: SearchConfigurationServiceStub; + let paginationService: PaginationServiceStub; + + const searchLink = '/search'; + let appliedFilter: AppliedFilter; + let initialRouteParams: Params; + let pagination: PaginationComponentOptions; + + function init(): void { + appliedFilter = Object.assign(new AppliedFilter(), { + filter: 'author', + operator: 'authority', + value: '1282121b-5394-4689-ab93-78d537764052', + label: 'Odinson, Thor', + }); + initialRouteParams = { + 'query': '', + 'page-id.page': '5', + 'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator), + 'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'), + }; + pagination = Object.assign(new PaginationComponentOptions(), { + id: 'page-id', + currentPage: 1, + pageSize: 20, + }); + } + + beforeEach(waitForAsync(async () => { + init(); + route = new ActivatedRouteStub(initialRouteParams); + searchConfigurationService = new SearchConfigurationServiceStub(); + paginationService = new PaginationServiceStub(pagination); + + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + TranslateModule.forRoot(), + ], + declarations: [ + SearchLabelRangeComponent, + ], + providers: [ + { provide: PaginationService, useValue: paginationService }, + { provide: SearchConfigurationService, useValue: searchConfigurationService }, + { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, + { provide: ActivatedRoute, useValue: route }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchLabelRangeComponent); + comp = fixture.componentInstance; + comp.appliedFilter = appliedFilter; + fixture.detectChanges(); + }); + + describe('updateRemoveParams', () => { + 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(); + }); + }); + }); +}); diff --git a/src/app/shared/search/search-labels/search-label-range/search-label-range.component.ts b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.ts new file mode 100644 index 00000000000..838a42cbfec --- /dev/null +++ b/src/app/shared/search/search-labels/search-label-range/search-label-range.component.ts @@ -0,0 +1,79 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Params, Router } from '@angular/router'; +import { SearchService } from '../../../../core/shared/search/search.service'; +import { currentPath } from '../../../utils/route.utils'; +import { AppliedFilter } from '../../models/applied-filter.model'; +import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator'; +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({ + selector: 'ds-search-label-range', + templateUrl: './search-label-range.component.html', +}) +@renderSearchLabelFor('range') +export class SearchLabelRangeComponent implements OnInit { + + @Input() inPlaceSearch: boolean; + + @Input() appliedFilter: AppliedFilter; + + searchLink: string; + + removeParametersMin$: Observable; + + removeParametersMax$: Observable; + + min: string; + + max: string; + + constructor( + protected paginationService: PaginationService, + protected router: Router, + protected searchConfigurationService: SearchConfigurationService, + protected searchService: SearchService, + ) { + } + + ngOnInit(): void { + this.searchLink = this.getSearchLink(); + 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.removeParametersMin$ = this.updateRemoveParams(`${this.appliedFilter.filter}.min`, this.min); + 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 { + 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 + */ + getSearchLink(): string { + if (this.inPlaceSearch) { + return currentPath(this.router); + } + return this.searchService.getSearchLink(); + } + +} diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.html b/src/app/shared/search/search-labels/search-label/search-label.component.html index bffb7f9329b..f092526f5ed 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.html +++ b/src/app/shared/search/search-labels/search-label/search-label.component.html @@ -1,6 +1,6 @@ - {{('search.filters.applied.' + key) | translate}}: {{'search.filters.' + filterName + '.' + value | translate: {default: normalizeFilterValue(value)} }} + [queryParams]="(removeParameters$ | async)"> + {{('search.filters.applied.f.' + appliedFilter.filter) | translate}}: {{'search.filters.' + appliedFilter.filter + '.' + appliedFilter.label | translate: {default: appliedFilter.label} }} × diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts index b2be2ae53f0..96e4e9823b3 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts @@ -1,97 +1,94 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { Observable, of as observableOf } from 'rxjs'; -import { Params, Router } from '@angular/router'; +import { Params, ActivatedRoute } from '@angular/router'; import { SearchLabelComponent } from './search-label.component'; -import { ObjectKeysPipe } from '../../../utils/object-keys-pipe'; -import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; import { SearchServiceStub } from '../../../testing/search-service.stub'; -import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchService } from '../../../../core/shared/search/search.service'; -import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { ActivatedRouteStub } from '../../../testing/active-router.stub'; +import { AppliedFilter } from '../../models/applied-filter.model'; +import { addOperatorToFilterValue } from '../../search.utils'; +import { RouterTestingModule } from '@angular/router/testing'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +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', () => { let comp: SearchLabelComponent; let fixture: ComponentFixture; + let route: ActivatedRouteStub; + let searchConfigurationService: SearchConfigurationServiceStub; + let paginationService: PaginationServiceStub; + const searchLink = '/search'; - let searchService; + let appliedFilter: AppliedFilter; + let initialRouteParams: Params; + let pagination: PaginationComponentOptions; - const key1 = 'author'; - const key2 = 'subject'; - const value1 = 'Test, Author'; - const normValue1 = 'Test, Author'; - const value2 = 'TestSubject'; - const value3 = 'Test, Authority,authority'; - const normValue3 = 'Test, Authority'; - const filter1 = [key1, value1]; - const filter2 = [key2, value2]; - const mockFilters = [ - filter1, - filter2 - ]; + function init(): void { + appliedFilter = Object.assign(new AppliedFilter(), { + filter: 'author', + operator: 'authority', + value: '1282121b-5394-4689-ab93-78d537764052', + label: 'Odinson, Thor', + }); + initialRouteParams = { + 'query': '', + 'spc.page': '1', + 'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator), + 'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'), + }; + 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); + beforeEach(waitForAsync(async () => { + init(); + route = new ActivatedRouteStub(initialRouteParams); + searchConfigurationService = new SearchConfigurationServiceStub(); + paginationService = new PaginationServiceStub(pagination); - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], - declarations: [SearchLabelComponent, ObjectKeysPipe], + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + TranslateModule.forRoot(), + ], + declarations: [ + SearchLabelComponent, + ], providers: [ - { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, - { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, - { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: PaginationService, useValue: paginationService }, - { provide: Router, useValue: {} } - // { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} } + { provide: SearchConfigurationService, useValue: searchConfigurationService }, + { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, + { provide: ActivatedRoute, useValue: route }, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideComponent(SearchLabelComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(SearchLabelComponent); comp = fixture.componentInstance; - searchService = (comp as any).searchService; - comp.key = key1; - comp.value = value1; - (comp as any).appliedFilters = observableOf(mockFilters); + comp.appliedFilter = appliedFilter; fixture.detectChanges(); }); - describe('when getRemoveParams is called', () => { - let obs: Observable; - - beforeEach(() => { - obs = comp.getRemoveParams(); - }); + describe('updateRemoveParams', () => { + it('should always reset the page to 1', (done: DoneFn) => { + spyOn(searchConfigurationService, 'unselectAppliedFilterParams').and.returnValue(observableOf(initialRouteParams)); - it('should return all params but the provided filter', () => { - obs.subscribe((params) => { - // Should contain only filter2 and page: length == 2 - expect(Object.keys(params).length).toBe(2); + comp.updateRemoveParams().pipe(take(1)).subscribe((params: Params) => { + expect(params).toEqual(Object.assign({}, initialRouteParams, { + 'page-id.page': 1, + })); + done(); }); }); }); - - describe('when normalizeFilterValue is called', () => { - it('should return properly filter value', () => { - let result: string; - - result = comp.normalizeFilterValue(value1); - expect(result).toBe(normValue1); - - result = comp.normalizeFilterValue(value3); - expect(result).toBe(normValue3); - }); - }); }); diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.ts b/src/app/shared/search/search-labels/search-label/search-label.component.ts index ab4c57d9f53..524296b1d54 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.ts @@ -1,94 +1,71 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Params, Router } from '@angular/router'; -import { map } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../../../empty.util'; import { SearchService } from '../../../../core/shared/search/search.service'; import { currentPath } from '../../../utils/route.utils'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { AppliedFilter } from '../../models/applied-filter.model'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; -import { stripOperatorFromFilterValue } from '../../search.utils'; +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({ selector: 'ds-search-label', templateUrl: './search-label.component.html', }) - -/** - * Component that represents the label containing the currently active filters - */ +@renderSearchLabelFor('equals') +@renderSearchLabelFor('notequals') +@renderSearchLabelFor('authority') +@renderSearchLabelFor('notauthority') +@renderSearchLabelFor('contains') +@renderSearchLabelFor('notcontains') +@renderSearchLabelFor('query') export class SearchLabelComponent implements OnInit { - @Input() key: string; - @Input() value: string; @Input() inPlaceSearch: boolean; - @Input() appliedFilters: Observable; + @Input() appliedFilter: AppliedFilter; searchLink: string; - removeParameters: Observable; - - /** - * The name of the filter without the f. prefix - */ - filterName: string; + removeParameters$: Observable; /** * Initialize the instance variable */ constructor( - private searchService: SearchService, - private paginationService: PaginationService, - private searchConfigurationService: SearchConfigurationService, - private router: Router) { + protected paginationService: PaginationService, + protected router: Router, + protected searchConfigurationService: SearchConfigurationService, + protected searchService: SearchService, + ) { } ngOnInit(): void { this.searchLink = this.getSearchLink(); - this.removeParameters = this.getRemoveParams(); - this.filterName = this.getFilterName(); + this.removeParameters$ = this.updateRemoveParams(); } /** - * Calculates the parameters that should change if a given value for the given filter would be removed from the active filters - * @returns {Observable} The changed filter parameters + * Calculates the parameters that should change if this {@link appliedFilter} would be removed from the active filters */ - getRemoveParams(): Observable { - return this.appliedFilters.pipe( - map((filters) => { - const field: string = Object.keys(filters).find((f) => f === this.key); - const newValues = hasValue(filters[field]) ? filters[field].filter((v) => v !== this.value) : null; - const page = this.paginationService.getPageParam(this.searchConfigurationService.paginationID); - return { - [field]: isNotEmpty(newValues) ? newValues : null, - [page]: 1 - }; - }) + updateRemoveParams(): Observable { + 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 */ - private getSearchLink(): string { + getSearchLink(): string { if (this.inPlaceSearch) { return currentPath(this.router); } return this.searchService.getSearchLink(); } - /** - * TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved - * Strips authority operator from filter value - * e.g. 'test ,authority' => 'test' - * - * @param value - */ - normalizeFilterValue(value: string) { - // const pattern = /,[^,]*$/g; - const pattern = /,authority*$/g; - value = value.replace(pattern, ''); - return stripOperatorFromFilterValue(value); - } - - private getFilterName(): string { - return this.key.startsWith('f.') ? this.key.substring(2) : this.key; - } } diff --git a/src/app/shared/search/search-labels/search-labels.component.html b/src/app/shared/search/search-labels/search-labels.component.html index f6af072c517..1dbf910dfb1 100644 --- a/src/app/shared/search/search-labels/search-labels.component.html +++ b/src/app/shared/search/search-labels/search-labels.component.html @@ -1,5 +1,8 @@
- - - + + + +
diff --git a/src/app/shared/search/search-labels/search-labels.component.spec.ts b/src/app/shared/search/search-labels/search-labels.component.spec.ts index e00c56aabca..a8cd7cc58fb 100644 --- a/src/app/shared/search/search-labels/search-labels.component.spec.ts +++ b/src/app/shared/search/search-labels/search-labels.component.spec.ts @@ -16,7 +16,6 @@ describe('SearchLabelsComponent', () => { let fixture: ComponentFixture; const searchLink = '/search'; - let searchService; const field1 = 'author'; const field2 = 'subject'; @@ -46,16 +45,10 @@ describe('SearchLabelsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SearchLabelsComponent); comp = fixture.componentInstance; - searchService = (comp as any).searchService; - (comp as any).appliedFilters = observableOf(mockFilters); fixture.detectChanges(); }); - describe('when the component has been initialized', () => { - it('should return all params but the provided filter', () => { - comp.appliedFilters.subscribe((filters) => { - expect(filters).toBe(mockFilters); - }); - }); + it('should create', () => { + expect(comp).toBeTruthy(); }); }); diff --git a/src/app/shared/search/search-labels/search-labels.component.ts b/src/app/shared/search/search-labels/search-labels.component.ts index 2cc1919f500..388ce08e46e 100644 --- a/src/app/shared/search/search-labels/search-labels.component.ts +++ b/src/app/shared/search/search-labels/search-labels.component.ts @@ -1,9 +1,5 @@ -import { Component, Inject, Input } from '@angular/core'; -import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; -import { Observable } from 'rxjs'; -import { Params, Router } from '@angular/router'; -import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; -import { map } from 'rxjs/operators'; +import { Component, Input } from '@angular/core'; +import { AppliedFilter } from '../models/applied-filter.model'; @Component({ selector: 'ds-search-labels', @@ -15,31 +11,15 @@ import { map } from 'rxjs/operators'; * Component that represents the labels containing the currently active filters */ export class SearchLabelsComponent { - /** - * Emits the currently active filters - */ - appliedFilters: Observable; /** * True when the search component should show results on the current page */ - @Input() inPlaceSearch; + @Input() inPlaceSearch: boolean; /** - * Initialize the instance variable + * The {@link AppliedFilter}s by filter name */ - constructor( - protected router: Router, - @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) { - this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters().pipe( - map((params) => { - const labels = {}; - Object.keys(params) - .forEach((key) => { - labels[key] = [...params[key].map((value) => value)]; - }); - return labels; - }) - ); - } + @Input() appliedFilters: Map; + } diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.html b/src/app/shared/search/search-sidebar/search-sidebar.component.html index f489de5d289..c59617bafb9 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.html +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.html @@ -21,7 +21,9 @@ [currentConfiguration]="configuration" [filters]="filters" [refreshFilters]="refreshFilters" - [inPlaceSearch]="inPlaceSearch"> + [inPlaceSearch]="inPlaceSearch" + (changeAppliedFilters)="changeAppliedFilters.emit($event)"> +
diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.ts b/src/app/shared/search/search-sidebar/search-sidebar.component.ts index 929f819ca3f..b5692e245ef 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.ts +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.ts @@ -7,6 +7,7 @@ import { SortOptions } from '../../../core/cache/models/sort-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { RemoteData } from '../../../core/data/remote-data'; import { SearchFilterConfig } from '../models/search-filter-config.model'; +import { AppliedFilter } from '../models/applied-filter.model'; /** * This component renders a simple item page. @@ -95,6 +96,11 @@ export class SearchSidebarComponent { */ @Output() changeConfiguration: EventEmitter = new EventEmitter(); + /** + * Emits the {@link AppliedFilter}s by search filter name + */ + @Output() changeAppliedFilters: EventEmitter> = new EventEmitter(); + /** * Emits event when the user select a new view mode */ diff --git a/src/app/shared/search/search.component.html b/src/app/shared/search/search.component.html index d43f506866f..e65d5858e49 100644 --- a/src/app/shared/search/search.component.html +++ b/src/app/shared/search/search.component.html @@ -61,6 +61,7 @@ [viewModeList]="viewModeList" [showViewModes]="showViewModes" (changeConfiguration)="changeContext($event.context)" + (changeAppliedFilters)="appliedFilters = $event" (changeViewMode)="changeViewMode()"> @@ -91,7 +93,8 @@
- + +
diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 61f3a119c87..ff7b7fc6528 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -34,10 +34,11 @@ import { CollectionElementLinkType } from '../object-collection/collection-eleme import { environment } from 'src/environments/environment'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; -import { WorkspaceItem } from '../..//core/submission/models/workspaceitem.model'; +import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; +import { AppliedFilter } from './models/applied-filter.model'; @Component({ selector: 'ds-search', @@ -268,6 +269,11 @@ export class SearchComponent implements OnInit { */ @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * The {@link AppliedFilter}s by filter name + */ + appliedFilters: Map = new Map(); + constructor(protected service: SearchService, protected sidebarService: SidebarService, protected windowService: HostWindowService, diff --git a/src/app/shared/search/search.module.ts b/src/app/shared/search/search.module.ts index 69500999a8c..261d16774d7 100644 --- a/src/app/shared/search/search.module.ts +++ b/src/app/shared/search/search.module.ts @@ -5,7 +5,10 @@ import { SearchFiltersComponent } from './search-filters/search-filters.componen import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component'; import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component'; import { SearchLabelsComponent } from './search-labels/search-labels.component'; +import { SearchLabelLoaderComponent } from './search-labels/search-label-loader/search-label-loader.component'; +import { SearchLabelLoaderDirective } from './search-labels/search-label-loader/search-label-loader-directive.directive'; import { SearchLabelComponent } from './search-labels/search-label/search-label.component'; +import { SearchLabelRangeComponent } from './search-labels/search-label-range/search-label-range.component'; import { SearchFacetFilterWrapperComponent } from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component'; import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component'; import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component'; @@ -35,7 +38,22 @@ import { NouisliderModule } from 'ng2-nouislider'; import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component'; import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component'; +const ENTRY_COMPONENTS = [ + SearchFacetFilterComponent, + SearchRangeFilterComponent, + SearchTextFilterComponent, + SearchHierarchyFilterComponent, + SearchBooleanFilterComponent, + SearchFacetOptionComponent, + SearchFacetSelectedOptionComponent, + SearchFacetRangeOptionComponent, + SearchAuthorityFilterComponent, + SearchLabelComponent, + SearchLabelRangeComponent, +]; + const COMPONENTS = [ + ...ENTRY_COMPONENTS, SearchComponent, ThemedSearchComponent, SearchResultsComponent, @@ -43,18 +61,9 @@ const COMPONENTS = [ SearchSettingsComponent, SearchFiltersComponent, SearchFilterComponent, - SearchFacetFilterComponent, SearchLabelsComponent, - SearchLabelComponent, + SearchLabelLoaderComponent, SearchFacetFilterWrapperComponent, - SearchRangeFilterComponent, - SearchTextFilterComponent, - SearchHierarchyFilterComponent, - SearchBooleanFilterComponent, - SearchFacetOptionComponent, - SearchFacetSelectedOptionComponent, - SearchFacetRangeOptionComponent, - SearchAuthorityFilterComponent, SearchSwitchConfigurationComponent, ConfigurationSearchPageComponent, ThemedConfigurationSearchPageComponent, @@ -64,18 +73,6 @@ const COMPONENTS = [ ThemedSearchSidebarComponent, ]; -const ENTRY_COMPONENTS = [ - SearchFacetFilterComponent, - SearchRangeFilterComponent, - SearchTextFilterComponent, - SearchHierarchyFilterComponent, - SearchBooleanFilterComponent, - SearchFacetOptionComponent, - SearchFacetSelectedOptionComponent, - SearchFacetRangeOptionComponent, - SearchAuthorityFilterComponent, -]; - /** * Declaration needed to make sure all decorator functions are called in time */ @@ -88,7 +85,8 @@ export const MODELS = [ @NgModule({ declarations: [ - ...COMPONENTS + ...COMPONENTS, + SearchLabelLoaderDirective, ], imports: [ CommonModule, @@ -100,7 +98,8 @@ export const MODELS = [ NouisliderModule, ], exports: [ - ...COMPONENTS + ...COMPONENTS, + SearchLabelLoaderDirective, ] }) export class SearchModule { diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index 78b358f0d47..fb72310417b 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -1,6 +1,5 @@ -import { BehaviorSubject, of as observableOf } from 'rxjs'; -import { SearchConfig } from '../../core/shared/search/search-filters/search-config.model'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { BehaviorSubject, of as observableOf, Observable } from 'rxjs'; +import { Params } from '@angular/router'; export class SearchConfigurationServiceStub { @@ -33,15 +32,12 @@ export class SearchConfigurationServiceStub { return observableOf([{value: 'test', label: 'test'}]); } - getConfigurationSearchConfigObservable() { - return observableOf(new SearchConfig()); + unselectAppliedFilterParams(_filterName: string, _value: string, _operator?: string): Observable { + return observableOf({}); } - getConfigurationSortOptionsObservable() { - return observableOf([new SortOptions('score', SortDirection.ASC), new SortOptions('score', SortDirection.DESC)]); + selectNewAppliedFilterParams(_filterName: string, _value: string, _operator?: string): Observable { + return observableOf({}); } - initializeSortOptionsFromConfiguration() { - /* empty */ - } } diff --git a/src/app/shared/testing/search-filter-service.stub.ts b/src/app/shared/testing/search-filter-service.stub.ts new file mode 100644 index 00000000000..f732c98fb82 --- /dev/null +++ b/src/app/shared/testing/search-filter-service.stub.ts @@ -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 { + return observableOf(true); + } + + isFilterActive(_paramName: string): Observable { + return observableOf(true); + } + + getCurrentScope(): Observable { + return observableOf(undefined); + } + + getCurrentQuery(): Observable { + return observableOf(undefined); + } + + getCurrentPagination(_pagination: any = {}): Observable { + return Object.assign(new PaginationComponentOptions()); + } + + getCurrentSort(_defaultSort: SortOptions): Observable { + return observableOf(new SortOptions('', SortDirection.ASC)); + } + + getCurrentFilters(): Observable { + return observableOf({}); + } + + getCurrentView(): Observable { + return observableOf(undefined); + } + + getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable { + return observableOf([]); + } + + isCollapsed(_filterName: string): Observable { + return observableOf(true); + } + + getPage(_filterName: string): Observable { + 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 { + } + +} diff --git a/src/app/shared/testing/search-service.stub.ts b/src/app/shared/testing/search-service.stub.ts index b64fdd13306..9a47c666c9e 100644 --- a/src/app/shared/testing/search-service.stub.ts +++ b/src/app/shared/testing/search-service.stub.ts @@ -1,5 +1,7 @@ import {of as observableOf, Observable , BehaviorSubject } from 'rxjs'; import { ViewMode } from '../../core/shared/view-mode.model'; +import { SearchFilterConfig } from '../search/models/search-filter-config.model'; +import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; export class SearchServiceStub { @@ -20,7 +22,7 @@ export class SearchServiceStub { this.testViewMode = viewMode; } - getFacetValuesFor() { + getFacetValuesFor(_filterConfig: SearchFilterConfig, _valuePage: number, _searchOptions?: PaginatedSearchOptions, _filterQuery?: string, _useCachedVersionIfAvailable = true) { return null; }