From 1f42f923444057249a2f3c693c205bb173feb485 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 7 May 2024 10:32:55 +0200 Subject: [PATCH] 111731: Hide the search facets when there are no facet suggestions & the applied filters of that facet don't have the operator equals, authority or range (because those should be displayed in the facets) --- .../search-facet-filter.component.spec.ts | 75 ++++++++++++----- .../search-facet-filter.component.ts | 14 +++- .../search-filter.component.spec.ts | 81 +++++++++++++++++-- .../search-filter/search-filter.component.ts | 5 +- .../search-configuration-service.stub.ts | 6 +- 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index 61c9f50c92..6d7c09875b 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -24,6 +24,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils import { AppliedFilter } from '../../../models/applied-filter.model'; import { FacetValues } from '../../../models/facet-values.model'; import { SearchFilterServiceStub } from '../../../../testing/search-filter-service.stub'; +import { cold } from 'jasmine-marbles'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; describe('SearchFacetFilterComponent', () => { let comp: SearchFacetFilterComponent; @@ -32,6 +34,7 @@ describe('SearchFacetFilterComponent', () => { const value1 = 'testvalue1'; const value2 = 'test2'; const value3 = 'another value3'; + const value4 = '52d629dc-7d2f-47b9-aa2d-258b92e45ae1'; const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), { name: filterName1, filterType: FilterType.text, @@ -39,27 +42,39 @@ describe('SearchFacetFilterComponent', () => { isOpenByDefault: false, pageSize: 2 }); + const appliedFilter1: AppliedFilter = Object.assign(new AppliedFilter(), { + filter: filterName1, + operator: 'equals', + label: value1, + value: value1, + }); + const appliedFilter2: AppliedFilter = Object.assign(new AppliedFilter(), { + filter: filterName1, + operator: 'equals', + label: value2, + value: value2, + }); + const appliedFilter3: AppliedFilter = Object.assign(new AppliedFilter(), { + filter: filterName1, + operator: 'equals', + label: value3, + value: value3, + }); + const appliedFilter4: AppliedFilter = Object.assign(new AppliedFilter(), { + filter: filterName1, + operator: 'notauthority', + label: value4, + value: value4, + }); const values: Partial = { appliedFilters: [ - { - filter: filterName1, - operator: 'equals', - label: value1, - value: value1, - }, - { - filter: filterName1, - operator: 'equals', - label: value2, - value: value2, - }, - { - filter: filterName1, - operator: 'equals', - label: value3, - value: value3, - } - ] + appliedFilter1, + appliedFilter2, + appliedFilter3, + ], + pageInfo: Object.assign(new PageInfo(), { + currentPage: 0, + }), }; const searchLink = '/search'; @@ -205,4 +220,26 @@ describe('SearchFacetFilterComponent', () => { expect(comp.filter).toEqual(''); }); }); + + describe('when new values are detected for a filter', () => { + let selectedValues$: BehaviorSubject; + + beforeEach(() => { + selectedValues$ = new BehaviorSubject([appliedFilter1, appliedFilter2, appliedFilter3]); + spyOn(searchService, 'getSelectedValuesForFilter').and.returnValue(selectedValues$); + comp.ngOnInit(); + }); + + it('should updated the selectedAppliedFilters$ when they are AppliedFilters that should be displayed in the search facets', () => { + expect(comp.selectedAppliedFilters$).toBeObservable(cold('a', { + a: [appliedFilter1, appliedFilter2, appliedFilter3], + })); + + selectedValues$.next([appliedFilter1, appliedFilter2, appliedFilter3, appliedFilter4]); + + expect(comp.selectedAppliedFilters$).toBeObservable(cold('a', { + a: [appliedFilter1, appliedFilter2, appliedFilter3], + })); + }); + }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index e085d3e667..b7edf6069e 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -20,6 +20,15 @@ import { currentPath } from '../../../../utils/route.utils'; import { FacetValues } from '../../../models/facet-values.model'; import { AppliedFilter } from '../../../models/applied-filter.model'; +/** + * The operators the {@link AppliedFilter} should have in order to be shown in the facets + */ +export const FACET_OPERATORS: string[] = [ + 'equals', + 'authority', + 'range', +]; + @Component({ selector: 'ds-search-facet-filter', template: ``, @@ -104,7 +113,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.searchOptions$.subscribe(() => this.updateFilterValueList()), this.retrieveFilterValues().subscribe(), ); - this.selectedAppliedFilters$ = this.searchService.getSelectedValuesForFilter(this.filterConfig.name); + this.selectedAppliedFilters$ = this.searchService.getSelectedValuesForFilter(this.filterConfig.name).pipe( + map((allAppliedFilters: AppliedFilter[]) => allAppliedFilters.filter((appliedFilter: AppliedFilter) => FACET_OPERATORS.includes(appliedFilter.operator))), + distinctUntilChanged((previous: AppliedFilter[], next: AppliedFilter[]) => JSON.stringify(previous) === JSON.stringify(next)), + ); } /** diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts index 3404d3def8..098bbe6d9d 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -16,10 +16,22 @@ import { SequenceService } from '../../../../core/shared/sequence.service'; import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe'; import { SearchServiceStub } from '../../../testing/search-service.stub'; import { SearchFilterServiceStub } from '../../../testing/search-filter-service.stub'; +import { cold } from 'jasmine-marbles'; +import { AppliedFilter } from '../../models/applied-filter.model'; +import { FacetValues } from '../../models/facet-values.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; describe('SearchFilterComponent', () => { let comp: SearchFilterComponent; let fixture: ComponentFixture; + + const appliedFilter1: AppliedFilter = Object.assign(new AppliedFilter(), { + operator: 'equals', + }); + const appliedFilter2: AppliedFilter = Object.assign(new AppliedFilter(), { + operator: 'notauthority', + }); + const filterName1 = 'test name'; const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), { @@ -30,16 +42,21 @@ describe('SearchFilterComponent', () => { }); let searchFilterService: SearchFilterServiceStub; let sequenceService; - const mockResults = observableOf(['test', 'data']); let searchService: SearchServiceStub; + let searchConfigurationService: SearchConfigurationServiceStub; beforeEach(waitForAsync(() => { searchFilterService = new SearchFilterServiceStub(); searchService = new SearchServiceStub(); + searchConfigurationService = new SearchConfigurationServiceStub(); sequenceService = jasmine.createSpyObj('sequenceService', { next: 17 }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule], + imports: [ + NoopAnimationsModule, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], declarations: [ SearchFilterComponent, BrowserOnlyMockPipe, @@ -47,7 +64,7 @@ describe('SearchFilterComponent', () => { providers: [ { provide: SearchService, useValue: searchService }, { provide: SearchFilterService, useValue: searchFilterService }, - { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, + { provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigurationService }, { provide: SequenceService, useValue: sequenceService }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -57,7 +74,6 @@ describe('SearchFilterComponent', () => { })); beforeEach(() => { - spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockResults); fixture = TestBed.createComponent(SearchFilterComponent); comp = fixture.componentInstance; // SearchPageComponent test instance comp.filter = mockFilterConfig; @@ -121,4 +137,59 @@ describe('SearchFilterComponent', () => { sub.unsubscribe(); }); }); + + describe('isActive', () => { + it('should return true when there are facet value suggestions & no valid applied values', () => { + spyOn(searchService, 'getFacetValuesFor').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new FacetValues(), { + pageInfo: { + totalElements: 5, + }, + } as FacetValues))); + comp.appliedFilters$ = observableOf([appliedFilter2]); + + expect(comp.isActive()).toBeObservable(cold('(tt)', { + t: true, + })); + }); + + it('should return false when there are no facet value suggestions & no valid applied values', () => { + spyOn(searchService, 'getFacetValuesFor').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new FacetValues(), { + pageInfo: { + totalElements: 0, + }, + } as FacetValues))); + comp.appliedFilters$ = observableOf([appliedFilter2]); + + expect(comp.isActive()).toBeObservable(cold('(tf)', { + t: true, + f: false, + })); + }); + + it('should return true when there are no facet value suggestions & but there are valid applied values', () => { + spyOn(searchService, 'getFacetValuesFor').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new FacetValues(), { + pageInfo: { + totalElements: 0, + }, + } as FacetValues))); + comp.appliedFilters$ = observableOf([appliedFilter1, appliedFilter2]); + + expect(comp.isActive()).toBeObservable(cold('(tt)', { + t: true, + })); + }); + + it('should return true when there are facet value suggestions & there are valid applied values', () => { + spyOn(searchService, 'getFacetValuesFor').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new FacetValues(), { + pageInfo: { + totalElements: 5, + }, + } as FacetValues))); + comp.appliedFilters$ = observableOf([appliedFilter1, appliedFilter2]); + + expect(comp.isActive()).toBeObservable(cold('(tt)', { + t: true, + })); + }); + }); }); 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 e595187dc1..6c08b0644a 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 @@ -15,6 +15,7 @@ import { FacetValues } from '../../models/facet-values.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { AppliedFilter } from '../../models/applied-filter.model'; import { SearchOptions } from '../../models/search-options.model'; +import { FACET_OPERATORS } from './search-facet-filter/search-facet-filter.component'; @Component({ selector: 'ds-search-filter', @@ -166,13 +167,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy { * Check if a given filter is supposed to be shown or not * @returns {Observable} Emits true whenever a given filter config should be shown */ - private isActive(): Observable { + isActive(): Observable { return combineLatest([ this.appliedFilters$, this.searchConfigService.searchOptions, ]).pipe( switchMap(([selectedValues, options]: [AppliedFilter[], SearchOptions]) => { - if (isNotEmpty(selectedValues)) { + if (isNotEmpty(selectedValues.filter((appliedFilter: AppliedFilter) => FACET_OPERATORS.includes(appliedFilter.operator)))) { return observableOf(true); } else { return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe( diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index cc874a0e4f..f1985b5c82 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -4,6 +4,8 @@ import { FilterConfig, SearchConfig, } from '../../core/shared/search/search-filters/search-config.model'; +import { SearchOptions } from '../search/models/search-options.model'; +import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; /** * Stub class of {@link SearchConfigurationService} @@ -12,8 +14,8 @@ export class SearchConfigurationServiceStub { public paginationID = 'test-id'; - private searchOptions: BehaviorSubject = new BehaviorSubject({}); - private paginatedSearchOptions: BehaviorSubject = new BehaviorSubject({}); + public searchOptions: BehaviorSubject = new BehaviorSubject(new SearchOptions({})); + public paginatedSearchOptions: BehaviorSubject = new BehaviorSubject(new PaginatedSearchOptions({})); getCurrentFrontendFilters() { return observableOf([]);