diff --git a/src/app/+search-page/search-labels/search-labels.component.html b/src/app/+search-page/search-labels/search-labels.component.html new file mode 100644 index 0000000000..e68bb7d7bd --- /dev/null +++ b/src/app/+search-page/search-labels/search-labels.component.html @@ -0,0 +1,10 @@ +
+
+ + {{label.value}} + × + +
+
diff --git a/src/app/+search-page/search-labels/search-labels.component.spec.ts b/src/app/+search-page/search-labels/search-labels.component.spec.ts new file mode 100644 index 0000000000..60ba8d519a --- /dev/null +++ b/src/app/+search-page/search-labels/search-labels.component.spec.ts @@ -0,0 +1,66 @@ +import { SearchLabelsComponent } from './search-labels.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchService } from '../search-service/search.service'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SearchServiceStub } from '../../shared/testing/search-service-stub'; +import { Observable } from 'rxjs/Observable'; +import { FilterLabel } from '../search-service/filter-label.model'; +import { Params } from '@angular/router'; + +describe('SearchLabelsComponent', () => { + let comp: SearchLabelsComponent; + let fixture: ComponentFixture; + + const searchLink = '/search'; + let searchService; + + const field1 = 'author'; + const field2 = 'subject'; + const value1 = 'TestAuthor'; + const value2 = 'TestSubject'; + const filter1 = new FilterLabel(value1, field1); + const filter2 = new FilterLabel(value2, field2); + const mockFilters = [ + filter1, + filter2 + ]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], + declarations: [SearchLabelsComponent], + providers: [ + { provide: SearchService, useValue: new SearchServiceStub(searchLink) } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(SearchLabelsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchLabelsComponent); + comp = fixture.componentInstance; + searchService = (comp as any).searchService; + (comp as any).appliedFilters = Observable.of(mockFilters); + fixture.detectChanges(); + }); + + describe('when getRemoveParams is called', () => { + let obs: Observable; + + beforeEach(() => { + obs = comp.getRemoveParams(filter1); + }); + + 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); + }); + }) + }); +}); diff --git a/src/app/+search-page/search-labels/search-labels.component.ts b/src/app/+search-page/search-labels/search-labels.component.ts new file mode 100644 index 0000000000..4d6e3c44a8 --- /dev/null +++ b/src/app/+search-page/search-labels/search-labels.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { SearchService } from '../search-service/search.service'; +import { Observable } from 'rxjs/Observable'; +import { Params } from '@angular/router'; +import { FilterLabel } from '../search-service/filter-label.model'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'ds-search-labels', + templateUrl: './search-labels.component.html', +}) + +export class SearchLabelsComponent { + appliedFilters: Observable; + + constructor(private searchService: SearchService) { + this.appliedFilters = this.searchService.getFilterLabels(); + } + + getRemoveParams(filterLabel: FilterLabel): Observable { + return this.appliedFilters.pipe( + map((filters) => { + const values = []; + filters.forEach((filter) => { + if (filter.field === filterLabel.field && filter.value !== filterLabel.value) { + values.push(filter.value); + } + }); + return { + [filterLabel.field]: values, + page: 1 + }; + }) + ); + } + + getSearchLink() { + return this.searchService.getSearchLink(); + } +} diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index e8dee94139..6aa3e92bf3 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -10,6 +10,7 @@ [currentUrl]="getSearchLink()" [scopes]="(scopeListRD$ | async)?.payload?.page"> +
{ expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl); }); }); + + describe('when getFilterLabels is called', () => { + let obs: Observable; + const value = 'Test'; + const orgField = 'author'; + const field = 'f.' + orgField; + const mockConfig = new RemoteData(false, false, true, null, [ + { + name: orgField, + type: null, + hasFacets: false, + pageSize: 5, + isOpenByDefault: false, + paramName: field + } as SearchFilterConfig + ]); + const mockParams = []; + + beforeEach(() => { + spyOn((searchService as any), 'getConfig').and.returnValue(Observable.of(mockConfig)); + mockParams[field] = value; + (searchService as any).route.queryParams = Observable.of(mockParams); + obs = searchService.getFilterLabels(); + }); + + it('should return the correct labels', () => { + obs.subscribe((filters) => { + expect(filters[0].value).toEqual(value); + }); + }); + }); }); }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 774d2287e2..e27b43597b 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,6 +1,6 @@ import { Injectable, OnDestroy } from '@angular/core'; import { - ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router, + ActivatedRoute, NavigationExtras, Params, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -40,7 +40,8 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-o import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service'; import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; -import { observable } from 'rxjs/symbol/observable'; +import { FilterLabel } from './filter-label.model'; +import { combineLatest } from 'rxjs/observable/combineLatest'; @Injectable() export class SearchService implements OnDestroy { @@ -223,6 +224,27 @@ export class SearchService implements OnDestroy { return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } + getFilterLabels(): Observable { + return combineLatest(this.getConfig(), this.route.queryParams).pipe( + map(([rd, params]) => { + const filterLabels: FilterLabel[] = []; + rd.payload.forEach((config: SearchFilterConfig) => { + const param = params[config.paramName]; + if (param !== undefined) { + if (param instanceof Array && param.length > 1) { + param.forEach((p: string) => { + filterLabels.push(new FilterLabel(p, config.paramName)) + }); + } else { + filterLabels.push(new FilterLabel(param, config.paramName)); + } + } + }); + return filterLabels.filter((n) => n !== undefined && n.value.length > 0); + }) + ); + } + getViewMode(): Observable { return this.route.queryParams.map((params) => { if (isNotEmpty(params.view) && hasValue(params.view)) { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 72f5227e64..4ddfd8017c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -53,6 +53,7 @@ import { DebounceDirective } from './utils/debounce.directive'; import { ClickOutsideDirective } from './utils/click-outside.directive'; import { EmphasizePipe } from './utils/emphasize.pipe'; import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component'; +import { CapitalizePipe } from './utils/capitalize.pipe'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -71,7 +72,8 @@ const PIPES = [ FileSizePipe, SafeUrlPipe, TruncatePipe, - EmphasizePipe + EmphasizePipe, + CapitalizePipe ]; const COMPONENTS = [ diff --git a/src/app/shared/testing/search-service-stub.ts b/src/app/shared/testing/search-service-stub.ts index 7ad0d871ce..6d636b3743 100644 --- a/src/app/shared/testing/search-service-stub.ts +++ b/src/app/shared/testing/search-service-stub.ts @@ -37,4 +37,8 @@ export class SearchServiceStub { getSearchLink() { return this.searchLink; } + + getFilterLabels() { + return Observable.of([]); + } } diff --git a/src/app/shared/utils/capitalize.pipe.ts b/src/app/shared/utils/capitalize.pipe.ts new file mode 100644 index 0000000000..454eb5d845 --- /dev/null +++ b/src/app/shared/utils/capitalize.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core' + +/** + * Pipe to truncate a value in Angular. (Take a substring, starting at 0) + * Default value: 10 + */ +@Pipe({ + name: 'dsCapitalize' +}) +export class CapitalizePipe implements PipeTransform { + transform(value: string, args: string[]): string { + if (value) { + return value.charAt(0).toUpperCase() + value.slice(1); + } + return value; + } + +}