diff --git a/lib/content-services/i18n/en.json b/lib/content-services/i18n/en.json index d37572cbcd..753ff4c3d3 100644 --- a/lib/content-services/i18n/en.json +++ b/lib/content-services/i18n/en.json @@ -176,7 +176,8 @@ "APPLY": "Apply", "CLEAR-ALL": "Clear all", "SHOW-MORE": "Show more", - "SHOW-LESS": "Show less" + "SHOW-LESS": "Show less", + "FILTER-CATEGORY": "Filter category" }, "RANGE": { "FROM": "From", diff --git a/lib/content-services/search/components/search-filter/models/response-facet-query-list.model.ts b/lib/content-services/search/components/search-filter/models/response-facet-query-list.model.ts index c406f4c499..6dfad9a7be 100644 --- a/lib/content-services/search/components/search-filter/models/response-facet-query-list.model.ts +++ b/lib/content-services/search/components/search-filter/models/response-facet-query-list.model.ts @@ -19,17 +19,25 @@ import { ResponseFacetQuery } from '../../../facet-query.interface'; import { SearchFilterList } from './search-filter-list.model'; export class ResponseFacetQueryList extends SearchFilterList { - constructor(items: ResponseFacetQuery[] = [], pageSize: number = 5) { - const filtered = items - .filter(item => { - return item.count > 0; - }) - .map(item => { - return { ...item }; - }); + super( + items + .filter(item => { + return item.count > 0; + }) + .map(item => { + return { ...item }; + }), + pageSize + ); - super(filtered, pageSize); + this.filter = (query: ResponseFacetQuery) => { + if (this.filterText && query.label) { + const pattern = (this.filterText || '').toLowerCase(); + const label = query.label.toLowerCase(); + return label.startsWith(pattern); + } + return true; + }; } - } diff --git a/lib/content-services/search/components/search-filter/models/search-filter-list.model.spec.ts b/lib/content-services/search/components/search-filter/models/search-filter-list.model.spec.ts new file mode 100644 index 0000000000..447a5dae0e --- /dev/null +++ b/lib/content-services/search/components/search-filter/models/search-filter-list.model.spec.ts @@ -0,0 +1,200 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SearchFilterList } from './search-filter-list.model'; + +export class Payload { + name: string; + + constructor(public id: number) { + this.name = `Payload_${id}`; + } +} + +describe('SearchFilterList', () => { + + function generateItems(count: number): Payload[] { + return Array(count).fill(null).map((_, id) => new Payload(id)); + } + + it('should init with external items', () => { + const items = [ + new Payload(1), + new Payload(2) + ]; + const list = new SearchFilterList(items); + + expect(list.length).toBe(2); + expect(list.items[0]).toBe(items[0]); + expect(list.items[1]).toBe(items[1]); + }); + + it('should init with default values', () => { + const list = new SearchFilterList(); + + expect(list.items).toEqual([]); + expect(list.pageSize).toEqual(5); + expect(list.currentPageSize).toEqual(5); + }); + + it('should init with custom page size', () => { + const list = new SearchFilterList([], 10); + + expect(list.pageSize).toEqual(10); + expect(list.currentPageSize).toEqual(10); + }); + + it('should allow showing more items', () => { + const items = generateItems(6); + const list = new SearchFilterList(items, 4); + + expect(list.canShowMoreItems).toBeTruthy(); + }); + + it('should now allow showing more items', () => { + const items = generateItems(6); + const list = new SearchFilterList(items, 6); + + expect(list.canShowMoreItems).toBeFalsy(); + }); + + it('should show second page', () => { + const items = generateItems(6); + const list = new SearchFilterList(items, 4); + + expect(list.canShowMoreItems).toBeTruthy(); + expect(list.visibleItems.length).toBe(4); + + list.showMoreItems(); + expect(list.currentPageSize).toBe(8); + expect(list.canShowMoreItems).toBeFalsy(); + expect(list.visibleItems.length).toBe(6); + }); + + it('should detect if content fits single page', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 5); + + expect(list.fitsPage).toBeTruthy(); + }); + + it('should detect if content exceeds single page', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 4); + + expect(list.fitsPage).toBeFalsy(); + }); + + it('should allow showing less items', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 4); + list.showMoreItems(); + + expect(list.canShowMoreItems).toBeFalsy(); + expect(list.canShowLessItems).toBeTruthy(); + }); + + it('should not allow showing less items for single page', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 5); + + expect(list.canShowLessItems).toBeFalsy(); + }); + + it('should clear the collection', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 5); + list.clear(); + + expect(list.items.length).toBe(0); + expect(list.visibleItems.length).toBe(0); + }); + + it('should reset page settings on clear', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 4); + list.showMoreItems(); + + expect(list.pageSize).toBe(4); + expect(list.currentPageSize).toBe(8); + + list.clear(); + expect(list.pageSize).toEqual(4); + expect(list.currentPageSize).toEqual(4); + expect(list.items.length).toBe(0); + }); + + it('should return visible portion of the page 1', () => { + const items = generateItems(5); + const list = new SearchFilterList(items, 4); + + expect(list.length).toBe(5); + expect(list.visibleItems.length).toBe(4); + + expect(list.visibleItems[0].id).toBe(0); + expect(list.visibleItems[1].id).toBe(1); + expect(list.visibleItems[2].id).toBe(2); + expect(list.visibleItems[3].id).toBe(3); + + expect(list.items[4].id).toBe(4); + }); + + it('should use custom filter', () => { + const items = generateItems(5); + items[0].name = 'custom'; + + const list = new SearchFilterList(items, 5); + expect(list.visibleItems.length).toBe(5); + + list.filter = (item: Payload): boolean => { + return item.name === 'custom'; + }; + expect(list.visibleItems.length).toBe(1); + }); + + it('should update filtered items on filter text change', () => { + const items = generateItems(5); + items[0].name = 'custom'; + + const list = new SearchFilterList(items, 5); + expect(list.visibleItems.length).toBe(5); + + list.filter = (item: Payload): boolean => { + if (list.filterText) { + return item.name.startsWith(list.filterText); + } + return true; + }; + expect(list.visibleItems.length).toBe(5); + + list.filterText = 'cus'; + expect(list.visibleItems.length).toBe(1); + expect(list.visibleItems[0].name).toEqual('custom'); + + list.filterText = 'P'; + expect(list.visibleItems.length).toBe(4); + }); + + it('should reset filter text on clear', () => { + const list = new SearchFilterList([], 5); + list.filterText = 'test'; + list.clear(); + + expect(list.filterText).toBe(''); + }); + +}); diff --git a/lib/content-services/search/components/search-filter/models/search-filter-list.model.ts b/lib/content-services/search/components/search-filter/models/search-filter-list.model.ts index 8a5d7b355d..17601b0c59 100644 --- a/lib/content-services/search/components/search-filter/models/search-filter-list.model.ts +++ b/lib/content-services/search/components/search-filter/models/search-filter-list.model.ts @@ -16,51 +16,95 @@ */ export class SearchFilterList implements Iterable { + + private filteredItems: T[] = []; + private _filterText: string = ''; + items: T[] = []; pageSize: number = 5; currentPageSize: number = 5; - get visibleItems(): T[] { - return this.items.slice(0, this.currentPageSize); + get filterText(): string { + return this._filterText; } + set filterText(value: string) { + this._filterText = value; + this.applyFilter(); + } + + private _filter: (item: T) => boolean = () => true; + + get filter(): (item: T) => boolean { + return this._filter; + } + + set filter(value: (item: T) => boolean ) { + this._filter = value; + this.applyFilter(); + } + + private applyFilter() { + if (this.filter) { + this.filteredItems = this.items.filter(this.filter); + } else { + this.filteredItems = this.items; + } + this.currentPageSize = this.pageSize; + } + + /** Returns visible portion of the items. */ + get visibleItems(): T[] { + return this.filteredItems.slice(0, this.currentPageSize); + } + + /** Returns entire collection length including items not displayed on the page. */ get length(): number { return this.items.length; } + /** Detects whether more items can be displayed. */ get canShowMoreItems(): boolean { - return this.items.length > this.currentPageSize; + return this.filteredItems.length > this.currentPageSize; } + /** Detects whether less items can be displayed. */ get canShowLessItems(): boolean { return this.currentPageSize > this.pageSize; } + /** Detects whether content fits single page. */ get fitsPage(): boolean { - return this.pageSize > this.items.length; + return this.pageSize >= this.filteredItems.length; } constructor(items: T[] = [], pageSize: number = 5) { this.items = items; + this.filteredItems = items; this.pageSize = pageSize; this.currentPageSize = pageSize; } + /** Display more items. */ showMoreItems() { if (this.canShowMoreItems) { this.currentPageSize += this.pageSize; } } + /** Display less items. */ showLessItems() { if (this.canShowLessItems) { this.currentPageSize -= this.pageSize; } } + /** Reset entire collection and page settings. */ clear() { this.currentPageSize = this.pageSize; this.items = []; + this.filteredItems = []; + this.filterText = ''; } [Symbol.iterator](): Iterator { diff --git a/lib/content-services/search/components/search-filter/search-filter.component.html b/lib/content-services/search/components/search-filter/search-filter.component.html index f13ef9567b..9a3036c270 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.html +++ b/lib/content-services/search/components/search-filter/search-filter.component.html @@ -21,12 +21,23 @@ {{ facetQueriesLabel | translate }} + + + +
- {{ query.label | translate }} ({{ query.count }}) + {{ query.label }} ({{ query.count }})
@@ -52,14 +63,27 @@ (opened)="onFacetFieldExpanded(field)" (closed)="onFacetFieldCollapsed(field)"> - {{ field.label | translate }} + {{ field.label }} + + + + + +
- {{ (bucket.display || bucket.label) | translate }} ({{ bucket.count }}) + {{ bucket.display || bucket.label }} ({{ bucket.count }})
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.scss b/lib/content-services/search/components/search-filter/search-filter.component.scss index 9993c22e3c..b54136eeb8 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.scss +++ b/lib/content-services/search/components/search-filter/search-filter.component.scss @@ -7,6 +7,14 @@ } } +.facet-result-filter { + width: 100%; + + input > { + width: 100%; + } +} + .facet-buttons { text-align: right; diff --git a/lib/content-services/search/components/search-filter/search-filter.component.spec.ts b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts index 1e6159775b..a5e7bebc1c 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.spec.ts +++ b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts @@ -18,7 +18,7 @@ import { SearchFilterComponent } from './search-filter.component'; import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchConfiguration } from '../../search-configuration.interface'; -import { AppConfigService } from '@alfresco/adf-core'; +import { AppConfigService, TranslationMock } from '@alfresco/adf-core'; import { Subject } from 'rxjs/Subject'; import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; @@ -36,7 +36,8 @@ describe('SearchSettingsComponent', () => { const searchMock: any = { dataLoaded: new Subject() }; - component = new SearchFilterComponent(queryBuilder, searchMock); + const translationMock = new TranslationMock(); + component = new SearchFilterComponent(queryBuilder, searchMock, translationMock); component.ngOnInit(); }); @@ -118,6 +119,7 @@ describe('SearchSettingsComponent', () => { }); it('should unselect facet query and update builder', () => { + const translationMock = new TranslationMock(); const config: SearchConfiguration = { categories: [], facetQueries: { @@ -128,7 +130,7 @@ describe('SearchSettingsComponent', () => { }; appConfig.config.search = config; queryBuilder = new SearchQueryBuilderService(appConfig, null); - component = new SearchFilterComponent(queryBuilder, null); + component = new SearchFilterComponent(queryBuilder, null, translationMock); spyOn(queryBuilder, 'update').and.stub(); queryBuilder.filterQueries = [{ query: 'query1' }]; diff --git a/lib/content-services/search/components/search-filter/search-filter.component.ts b/lib/content-services/search/components/search-filter/search-filter.component.ts index 7c8c9810f7..c45b0b5173 100644 --- a/lib/content-services/search/components/search-filter/search-filter.component.ts +++ b/lib/content-services/search/components/search-filter/search-filter.component.ts @@ -17,7 +17,7 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material'; -import { SearchService } from '@alfresco/adf-core'; +import { SearchService, TranslationService } from '@alfresco/adf-core'; import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { ResponseFacetField } from '../../response-facet-field.interface'; import { FacetFieldBucket } from '../../facet-field-bucket.interface'; @@ -44,7 +44,9 @@ export class SearchFilterComponent implements OnInit { facetQueriesPageSize = 5; facetQueriesExpanded = false; - constructor(public queryBuilder: SearchQueryBuilderService, private search: SearchService) { + constructor(public queryBuilder: SearchQueryBuilderService, + private searchService: SearchService, + private translationService: TranslationService) { this.responseFacetQueries = new ResponseFacetQueryList(); if (queryBuilder.config && queryBuilder.config.facetQueries) { @@ -62,7 +64,7 @@ export class SearchFilterComponent implements OnInit { if (this.queryBuilder) { this.queryBuilder.executed.subscribe(data => { this.onDataLoaded(data); - this.search.dataLoaded.next(data); + this.searchService.dataLoaded.next(data); }); } } @@ -95,7 +97,7 @@ export class SearchFilterComponent implements OnInit { } } else { query.$checked = false; - this.selectedFacetQueries = this.selectedFacetQueries.filter(q => q !== query.label); + this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== query.label); if (facetQuery) { this.queryBuilder.removeFilterQuery(facetQuery.query); @@ -127,7 +129,7 @@ export class SearchFilterComponent implements OnInit { unselectFacetQuery(label: string) { const facetQuery = this.queryBuilder.getFacetQuery(label); - this.selectedFacetQueries = this.selectedFacetQueries.filter(q => q !== label); + this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== label); this.queryBuilder.removeFilterQuery(facetQuery.query); this.queryBuilder.update(); @@ -136,7 +138,7 @@ export class SearchFilterComponent implements OnInit { unselectFacetBucket(bucket: FacetFieldBucket) { if (bucket) { const idx = this.selectedBuckets.findIndex( - b => b.$field === bucket.$field && b.label === bucket.label + selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label ); if (idx >= 0) { @@ -151,17 +153,19 @@ export class SearchFilterComponent implements OnInit { const context = data.list.context; if (context) { - const facetQueries = (context.facetQueries || []).map(q => { - q.$checked = this.selectedFacetQueries.includes(q.label); - return q; + const facetQueries = (context.facetQueries || []).map(query => { + query.label = this.translationService.instant(query.label); + query.$checked = this.selectedFacetQueries.includes(query.label); + return query; }); this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize); - const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label); + const expandedFields = this.responseFacetFields.filter(field => field.expanded).map(field => field.label); this.responseFacetFields = (context.facetsFields || []).map( field => { + field.label = this.translationService.instant(field.label); field.pageSize = field.pageSize || 5; field.currentPageSize = field.pageSize; field.expanded = expandedFields.includes(field.label); @@ -169,16 +173,29 @@ export class SearchFilterComponent implements OnInit { const buckets = (field.buckets || []).map(bucket => { bucket.$field = field.label; bucket.$checked = false; + bucket.display = this.translationService.instant(bucket.display); + bucket.label = this.translationService.instant(bucket.label); const previousBucket = this.selectedBuckets.find( - b => b.$field === bucket.$field && b.label === bucket.label + selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label ); if (previousBucket) { bucket.$checked = true; } return bucket; }); - field.buckets = new SearchFilterList(buckets, field.pageSize); + + const bucketList = new SearchFilterList(buckets, field.pageSize); + bucketList.filter = (bucket: FacetFieldBucket): boolean => { + if (bucket && bucketList.filterText) { + const pattern = (bucketList.filterText || '').toLowerCase(); + const label = (bucket.display || bucket.label || '').toLowerCase(); + return label.startsWith(pattern); + } + return true; + }; + + field.buckets = bucketList; return field; } ); diff --git a/lib/core/services/translation.service.spec.ts b/lib/core/services/translation.service.spec.ts index a113d3d707..9c81894423 100644 --- a/lib/core/services/translation.service.spec.ts +++ b/lib/core/services/translation.service.spec.ts @@ -105,4 +105,10 @@ describe('TranslationService', () => { }); }); + it('should return empty string for missing key when getting instant translations', () => { + expect(translationService.instant(null)).toEqual(''); + expect(translationService.instant('')).toEqual(''); + expect(translationService.instant(undefined)).toEqual(''); + }); + }); diff --git a/lib/core/services/translation.service.ts b/lib/core/services/translation.service.ts index 07673936cf..ac39c8e15a 100644 --- a/lib/core/services/translation.service.ts +++ b/lib/core/services/translation.service.ts @@ -128,6 +128,6 @@ export class TranslationService { * @returns Translated text */ instant(key: string | Array, interpolateParams?: Object): string | any { - return this.translate.instant(key, interpolateParams); + return key ? this.translate.instant(key, interpolateParams) : ''; } }