From c63184334f19d9fcc2f862e228a289fdeb121ab8 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Tue, 17 Jul 2018 20:18:52 +0100 Subject: [PATCH] [ADF-3365] search filter fixes (#3594) * preserve ordering of facet fields and queries * update tests * rework facet queries * rework facet management, update unit tests * remove unused interfaces * fix test * remove deprecated interfaces * expose selection for the chip list --- demo-shell/src/app.config.json | 26 +- .../search-chip-list.component.html | 10 +- .../models/response-facet-query-list.model.ts | 11 +- .../search-filter.component.html | 213 +++++---- .../search-filter.component.spec.ts | 406 ++++++------------ .../search-filter/search-filter.component.ts | 322 +++++++------- .../search/facet-field-bucket.interface.ts | 3 +- .../search/facet-field.interface.ts | 7 +- .../search/facet-query.interface.ts | 7 +- lib/content-services/search/public-api.ts | 2 - .../search/response-facet-field.interface.ts | 27 -- .../search/response-facet-query.interface.ts | 23 - .../search-query-builder.service.spec.ts | 2 +- .../search/search-query-builder.service.ts | 74 +++- tools/export-check/export-2.3.0.json | 16 - 15 files changed, 484 insertions(+), 665 deletions(-) delete mode 100644 lib/content-services/search/response-facet-field.interface.ts delete mode 100644 lib/content-services/search/response-facet-query.interface.ts diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 17742a5695..7f0ce5d49b 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -83,24 +83,24 @@ { "query": "NOT cm:creator:System" } ], "facetFields": [ - { "field": "content.mimetype", "mincount": 1, "label": "Type" }, - { "field": "content.size", "mincount": 1, "label": "Size" }, - { "field": "creator", "mincount": 1, "label": "Creator" }, - { "field": "modifier", "mincount": 1, "label": "Modifier" }, - { "field": "created", "mincount": 1, "label": "Created" } + { "field": "content.mimetype", "mincount": 1, "label": "1:Type" }, + { "field": "content.size", "mincount": 1, "label": "2:Size" }, + { "field": "creator", "mincount": 1, "label": "3:Creator" }, + { "field": "modifier", "mincount": 1, "label": "4:Modifier" }, + { "field": "created", "mincount": 1, "label": "5:Created" } ], "facetQueries": { "label": "My facet queries", "pageSize": 5, "queries": [ - { "query": "created:2018", "label": "Created This Year" }, - { "query": "content.mimetype", "label": "Type" }, - { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"}, - { "query": "content.size:[10240 TO 102400]", "label": "Size: small"}, - { "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" }, - { "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" }, - { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, - { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } + { "query": "created:2018", "label": "1.Created This Year" }, + { "query": "content.mimetype", "label": "2.Type" }, + { "query": "content.size:[0 TO 10240]", "label": "3.Size: xtra small"}, + { "query": "content.size:[10240 TO 102400]", "label": "4.Size: small"}, + { "query": "content.size:[102400 TO 1048576]", "label": "5.Size: medium" }, + { "query": "content.size:[1048576 TO 16777216]", "label": "6.Size: large" }, + { "query": "content.size:[16777216 TO 134217728]", "label": "7.Size: xtra large" }, + { "query": "content.size:[134217728 TO MAX]", "label": "8.Size: XX large" } ] }, "categories": [ diff --git a/lib/content-services/search/components/search-chip-list/search-chip-list.component.html b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html index c125531d05..7a85b8ea9e 100644 --- a/lib/content-services/search/components/search-chip-list/search-chip-list.component.html +++ b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html @@ -1,14 +1,14 @@ - + - {{ label | translate }} + (remove)="searchFilter.unselectFacetQuery(query)"> + {{ query.label | translate }} cancel - + { - constructor(items: ResponseFacetQuery[] = [], pageSize: number = 5) { +export class ResponseFacetQueryList extends SearchFilterList { + constructor(items: FacetQuery[] = [], pageSize: number = 5) { super( items .filter(item => { return item.count > 0; - }) - .map(item => { - return { ...item }; }), pageSize ); - this.filter = (query: ResponseFacetQuery) => { + this.filter = (query: FacetQuery) => { if (this.filterText && query.label) { const pattern = (this.filterText || '').toLowerCase(); const label = query.label.toLowerCase(); 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 82d3f566d4..1bbfa7c1c6 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 @@ -2,9 +2,7 @@ + [(expanded)]="category.expanded"> {{ category.name | translate }} @@ -17,115 +15,114 @@ - - - {{ facetQueriesLabel | translate }} - -
- - - + +
+
+ + + {{ query.label }} ({{ query.count }}) + + +
+
+ - -
-
- + + +
+
+
+ + + + + {{ field.label }} + + +
+ + + + +
+ +
- {{ query.label }} ({{ query.count }}) + *ngFor="let bucket of field.buckets" + [checked]="bucket.checked" + (change)="onToggleBucket($event, bucket)"> + {{ bucket.display || bucket.label }} ({{ bucket.count }}) - -
-
- - - -
-
+ - - - {{ field.label }} - - -
- - - - -
- -
- - {{ bucket.display || bucket.label }} ({{ bucket.count }}) - -
- -
- -
- -
- - - -
-
+ +
+ + + +
+ +
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 b10334aa8a..0202d555d7 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 @@ -17,15 +17,15 @@ import { SearchFilterComponent } from './search-filter.component'; import { SearchQueryBuilderService } from '../../search-query-builder.service'; -import { SearchConfiguration } from '../../search-configuration.interface'; import { AppConfigService, TranslationMock } from '@alfresco/adf-core'; import { Subject } from 'rxjs/Subject'; -import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; -import { ResponseFacetField } from '../../response-facet-field.interface'; -import { SearchFilterList } from './models/search-filter-list.model'; import { FacetFieldBucket } from '../../facet-field-bucket.interface'; +import { FacetQuery } from '../../facet-query.interface'; +import { FacetField } from '../../facet-field.interface'; +import { SearchFilterList } from './models/search-filter-list.model'; +import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; -describe('SearchSettingsComponent', () => { +describe('SearchFilterComponent', () => { let component: SearchFilterComponent; let queryBuilder: SearchQueryBuilderService; @@ -52,150 +52,60 @@ describe('SearchSettingsComponent', () => { expect(component.onDataLoaded).toHaveBeenCalledWith(data); }); - it('should update category model on expand', () => { - const category: any = { expanded: false }; - - component.onCategoryExpanded(category); - - expect(category.expanded).toBeTruthy(); - }); - - it('should update category model on collapse', () => { - const category: any = { expanded: true }; - - component.onCategoryCollapsed(category); - - expect(category.expanded).toBeFalsy(); - }); - - it('should update facet field model on expand', () => { - const field: any = { expanded: false }; - - component.onFacetFieldExpanded(field); - - expect(field.expanded).toBeTruthy(); - }); - - it('should update facet field model on collapse', () => { - const field: any = { expanded: true }; - - component.onFacetFieldCollapsed(field); - - expect(field.expanded).toBeFalsy(); - }); - it('should update bucket model and query builder on facet toggle', () => { spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough(); const event: any = { checked: true }; - const field: any = {}; - const bucket: any = { $checked: false, filterQuery: 'q1' }; + const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 }; - component.onFacetToggle(event, field, bucket); - - expect(component.selectedBuckets.length).toBe(1); - expect(component.selectedBuckets[0]).toEqual(bucket); - - expect(queryBuilder.filterQueries.length).toBe(1); - expect(queryBuilder.filterQueries[0].query).toBe('q1'); + component.onToggleBucket(event, bucket); + expect(bucket.checked).toBeTruthy(); + expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(bucket); expect(queryBuilder.update).toHaveBeenCalled(); }); it('should update bucket model and query builder on facet un-toggle', () => { spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough(); const event: any = { checked: false }; - const field: any = { label: 'f1' }; - const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' }; + const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 }; - component.selectedBuckets.push(bucket); - queryBuilder.addFilterQuery(bucket.filterQuery); - - component.onFacetToggle(event, field, bucket); - - expect(bucket.$checked).toBeFalsy(); - expect(component.selectedBuckets.length).toBe(0); - expect(queryBuilder.filterQueries.length).toBe(0); + component.onToggleBucket(event, bucket); + expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(bucket); expect(queryBuilder.update).toHaveBeenCalled(); }); it('should unselect facet query and update builder', () => { - const translationMock = new TranslationMock(); - const config: SearchConfiguration = { - categories: [], - facetQueries: { - queries: [ - { label: 'q1', query: 'query1' } - ] - } - }; - appConfig.config.search = config; - queryBuilder = new SearchQueryBuilderService(appConfig, null); - component = new SearchFilterComponent(queryBuilder, null, translationMock); - spyOn(queryBuilder, 'update').and.stub(); - queryBuilder.filterQueries = [{ query: 'query1' }]; - component.selectedFacetQueries = ['q1']; + spyOn(queryBuilder, 'removeUserFacetQuery').and.callThrough(); - component.unselectFacetQuery('q1'); + const event: any = { checked: false }; + const query: FacetQuery = { checked: true, label: 'q1', query: 'query1' }; - expect(component.selectedFacetQueries.length).toBe(0); - expect(queryBuilder.filterQueries.length).toBe(0); + component.onToggleFacetQuery(event, query); + expect(query.checked).toBeFalsy(); + expect(queryBuilder.removeUserFacetQuery).toHaveBeenCalledWith(query); expect(queryBuilder.update).toHaveBeenCalled(); }); - it('should unselect facet bucket and update builder', () => { - spyOn(queryBuilder, 'update').and.stub(); - - const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' }; - component.selectedBuckets.push(bucket); - queryBuilder.filterQueries.push({ query: 'q1' }); - - component.unselectFacetBucket(bucket); - - expect(component.selectedBuckets.length).toBe(0); - expect(queryBuilder.filterQueries.length).toBe(0); - - expect(queryBuilder.update).toHaveBeenCalled(); - }); - - it('should allow facetQueries when defined in configuration', () => { - component.queryBuilder.config = { - categories: [], - facetQueries: { - queries: [ - { label: 'q1', query: 'query1' } - ] - } - }; - - expect(component.isFacetQueriesDefined).toBe(true); - }); - - it('should not allow facetQueries when not defined in configuration', () => { - component.queryBuilder.config = { - categories: [] - }; - - expect(component.isFacetQueriesDefined).toBe(false); - }); - - it('should not allow facetQueries when queries are not defined in configuration', () => { - component.queryBuilder.config = { - categories: [], - facetQueries: { - queries: [] - } - }; - - expect(component.isFacetQueriesDefined).toBe(false); - }); - it('should fetch facet queries from response payload', () => { - component.responseFacetQueries = new ResponseFacetQueryList(); + component.responseFacetQueries = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + queries: [ + { label: 'q1', query: 'query1' }, + { label: 'q2', query: 'query2' } + ] + } + }; + const queries = [ { label: 'q1', query: 'query1', count: 1 }, { label: 'q2', query: 'query2', count: 1 } @@ -214,8 +124,51 @@ describe('SearchSettingsComponent', () => { expect(component.responseFacetQueries.items).toEqual(queries); }); + it('should preserve order after response processing', () => { + component.responseFacetQueries = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + queries: [ + { label: 'q1', query: 'query1' }, + { label: 'q2', query: 'query2' }, + { label: 'q3', query: 'query3' } + ] + } + }; + + const queries = [ + { label: 'q2', query: 'query2', count: 1 }, + { label: 'q1', query: 'query1', count: 1 }, + { label: 'q3', query: 'query3', count: 1 } + + ]; + const data = { + list: { + context: { + facetQueries: queries + } + } + }; + + component.onDataLoaded(data); + + expect(component.responseFacetQueries.length).toBe(3); + expect(component.responseFacetQueries.items[0].label).toBe('q1'); + expect(component.responseFacetQueries.items[1].label).toBe('q2'); + expect(component.responseFacetQueries.items[2].label).toBe('q3'); + }); + it('should not fetch facet queries from response payload', () => { - component.responseFacetQueries = new ResponseFacetQueryList(); + component.responseFacetQueries = null; + + queryBuilder.config = { + categories: [], + facetQueries: { + queries: [] + } + }; const data = { list: { @@ -227,57 +180,22 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); - expect(component.responseFacetQueries.length).toBe(0); - }); - - it('should restore checked state for new response facet queries', () => { - component.selectedFacetQueries = ['q3']; - component.responseFacetQueries = new ResponseFacetQueryList(); - - const queries = [ - { label: 'q1', query: 'query1', count: 1 }, - { label: 'q2', query: 'query2', count: 1 } - ]; - const data = { - list: { - context: { - facetQueries: queries - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacetQueries.length).toBe(2); - expect(( component.responseFacetQueries.items[0]).$checked).toBeFalsy(); - expect(( component.responseFacetQueries.items[1]).$checked).toBeFalsy(); - }); - - it('should not restore checked state for new response facet queries', () => { - component.selectedFacetQueries = ['q2']; - component.responseFacetQueries = new ResponseFacetQueryList(); - - const queries = [ - { label: 'q1', query: 'query1', count: 1 }, - { label: 'q2', query: 'query2', count: 1 } - ]; - const data = { - list: { - context: { - facetQueries: queries - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacetQueries.length).toBe(2); - expect(( component.responseFacetQueries.items[0]).$checked).toBeFalsy(); - expect(( component.responseFacetQueries.items[1]).$checked).toBeTruthy(); + expect(component.responseFacetQueries).toBeNull(); }); it('should fetch facet fields from response payload', () => { - component.responseFacetFields = []; + component.responseFacetFields = null; + + queryBuilder.config = { + categories: [], + facetFields: [ + { label: 'f1', field: 'f1' }, + { label: 'f2', field: 'f2' } + ], + facetQueries: { + queries: [] + } + }; const fields: any = [ { label: 'f1', buckets: [] }, @@ -293,95 +211,25 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); - expect(component.responseFacetFields).toEqual(fields); - }); - - it('should restore expanded state for new response facet fields', () => { - component.responseFacetFields = [ - { label: 'f1', buckets: [] }, - { label: 'f2', buckets: [], expanded: true } - ]; - - const fields = [ - { label: 'f1', buckets: [] }, - { label: 'f2', buckets: [] } - ]; - const data = { - list: { - context: { - facetsFields: fields - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacetFields.length).toBe(2); - expect(component.responseFacetFields[0].expanded).toBeFalsy(); - expect(component.responseFacetFields[1].expanded).toBeTruthy(); - }); - - it('should restore checked buckets for new response facet fields', () => { - const bucket1 = { label: 'b1', $field: 'f1', count: 1, filterQuery: 'q1' }; - const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' }; - - component.selectedBuckets = [bucket2]; - component.responseFacetFields = [ - { label: 'f2', buckets: [] } - ]; - - const data = { - list: { - context: { - facetsFields: [ - { label: 'f1', buckets: [bucket1] }, - { label: 'f2', buckets: [bucket2] } - ] - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacetFields.length).toBe(2); - expect(component.responseFacetFields[0].buckets.items[0].$checked).toBeFalsy(); - expect(component.responseFacetFields[1].buckets.items[0].$checked).toBeTruthy(); - }); - - it('should reset queries and fields on empty response payload', () => { - component.responseFacetQueries = new ResponseFacetQueryList([ {}, {}]); - component.responseFacetFields = [ {}, {}]; - - const data = { - list: { - context: { - facetQueries: null, - facetsFields: null - } - } - }; - - component.onDataLoaded(data); - - expect(component.responseFacetQueries.length).toBe(0); - expect(component.responseFacetFields.length).toBe(0); + expect(component.responseFacetFields.length).toEqual(2); }); it('should update query builder only when has bucket to unselect', () => { spyOn(queryBuilder, 'update').and.stub(); - component.unselectFacetBucket(null); + component.onToggleBucket( { checked: true }, null); expect(queryBuilder.update).not.toHaveBeenCalled(); }); it('should allow to to reset selected buckets', () => { const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', $checked: true, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', $checked: false, count: 1, filterQuery: 'q2' } + { label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } ]; - const field: ResponseFacetField = { + const field: FacetField = { + field: 'f1', label: 'field1', buckets: new SearchFilterList(buckets) }; @@ -391,11 +239,12 @@ describe('SearchSettingsComponent', () => { it('should not allow to reset selected buckets', () => { const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1' }, - { label: 'bucket2', $checked: false, count: 1, filterQuery: 'q2' } + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' } ]; - const field: ResponseFacetField = { + const field: FacetField = { + field: 'f1', label: 'field1', buckets: new SearchFilterList(buckets) }; @@ -405,70 +254,57 @@ describe('SearchSettingsComponent', () => { it('should reset selected buckets', () => { const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1', $field: 'field1' }, - { label: 'bucket2', $checked: true, count: 1, filterQuery: 'q2', $field: 'field1' } + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } ]; - const field: ResponseFacetField = { + const field: FacetField = { + field: 'f1', label: 'field1', buckets: new SearchFilterList(buckets) }; - component.selectedBuckets = [buckets[1]]; component.resetSelectedBuckets(field); - expect(buckets[0].$checked).toBeFalsy(); - expect(buckets[1].$checked).toBeFalsy(); - expect(component.selectedBuckets.length).toBe(0); + expect(buckets[0].checked).toBeFalsy(); + expect(buckets[1].checked).toBeFalsy(); }); it('should update query builder upon resetting buckets', () => { spyOn(queryBuilder, 'update').and.stub(); const buckets: FacetFieldBucket[] = [ - { label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1', $field: 'field1' }, - { label: 'bucket2', $checked: true, count: 1, filterQuery: 'q2', $field: 'field1' } + { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' }, + { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' } ]; - const field: ResponseFacetField = { + const field: FacetField = { + field: 'f1', label: 'field1', buckets: new SearchFilterList(buckets) }; - component.selectedBuckets = [buckets[1]]; component.resetSelectedBuckets(field); - expect(queryBuilder.update).toHaveBeenCalled(); }); - it('should allow to reset selected queries', () => { - component.selectedFacetQueries = ['q1', 'q2']; - expect(component.canResetSelectedQueries()).toBeTruthy(); - }); - - it('should not allow to reset selected queries when nothing selected', () => { - component.selectedFacetQueries = []; - expect(component.canResetSelectedQueries()).toBeFalsy(); - }); - - it('should reset selected queries', () => { - const methodSpy = spyOn(component, 'unselectFacetQuery').and.stub(); - - component.selectedFacetQueries = ['q1', 'q2']; - component.resetSelectedQueries(); - - expect(methodSpy.calls.count()).toBe(2); - expect(methodSpy.calls.argsFor(0)).toEqual(['q1', false]); - expect(methodSpy.calls.argsFor(1)).toEqual(['q2', false]); - }); - it('should update query builder upon resetting selected queries', () => { spyOn(queryBuilder, 'update').and.stub(); + spyOn(queryBuilder, 'removeUserFacetQuery').and.callThrough(); - component.selectedFacetQueries = ['q1', 'q2']; + component.canResetSelectedQueries = true; + component.responseFacetQueries = new ResponseFacetQueryList([ + { label: 'q1', query: 'q1', checked: true, count: 1 }, + { label: 'q2', query: 'q2', checked: false, count: 1 }, + { label: 'q3', query: 'q3', checked: true, count: 1 } + ]); component.resetSelectedQueries(); + expect(queryBuilder.removeUserFacetQuery).toHaveBeenCalledTimes(3); expect(queryBuilder.update).toHaveBeenCalled(); - }); + for (let entry of component.responseFacetQueries.items) { + expect(entry.checked).toBeFalsy(); + } + }); }); 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 a93cd6a91f..ea31ece5ab 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 @@ -19,11 +19,10 @@ import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core'; import { MatCheckboxChange } from '@angular/material'; 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'; -import { SearchCategory } from '../../search-category.interface'; -import { ResponseFacetQuery } from '../../response-facet-query.interface'; import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; +import { FacetQuery } from '../../facet-query.interface'; +import { FacetField } from '../../facet-field.interface'; import { SearchFilterList } from './models/search-filter-list.model'; @Component({ @@ -38,20 +37,20 @@ export class SearchFilterComponent implements OnInit, OnDestroy { private DEFAULT_PAGE_SIZE = 5; isAlive = true; - selectedFacetQueries: string[] = []; - selectedBuckets: FacetFieldBucket[] = []; - responseFacetQueries: ResponseFacetQueryList; - responseFacetFields: ResponseFacetField[] = []; + responseFacetQueries: ResponseFacetQueryList = null; + responseFacetFields: FacetField[] = null; + private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE; facetQueriesLabel: string = 'Facet Queries'; - facetQueriesPageSize = this.DEFAULT_PAGE_SIZE; facetQueriesExpanded = false; + canResetSelectedQueries = false; + + selectedFacetQueries: Array = []; + selectedBuckets: Array = []; constructor(public queryBuilder: SearchQueryBuilderService, private searchService: SearchService, private translationService: TranslationService) { - this.responseFacetQueries = new ResponseFacetQueryList(); - if (queryBuilder.config && queryBuilder.config.facetQueries) { this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries'; this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE; @@ -60,9 +59,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy { this.queryBuilder.updated .takeWhile(() => this.isAlive) - .subscribe(query => { - this.queryBuilder.execute(); - }); + .subscribe(() => { + this.queryBuilder.execute(); + }); } ngOnInit() { @@ -70,9 +69,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy { this.queryBuilder.executed .takeWhile(() => this.isAlive) .subscribe(data => { - this.onDataLoaded(data); - this.searchService.dataLoaded.next(data); - }); + this.onDataLoaded(data); + this.searchService.dataLoaded.next(data); + }); } } @@ -80,125 +79,111 @@ export class SearchFilterComponent implements OnInit, OnDestroy { this.isAlive = false; } - get isFacetQueriesDefined() { - return this.queryBuilder.hasFacetQueries; - } - - onCategoryExpanded(category: SearchCategory) { - category.expanded = true; - } - - onCategoryCollapsed(category: SearchCategory) { - category.expanded = false; - } - - onFacetFieldExpanded(field: ResponseFacetField) { - field.expanded = true; - } - - onFacetFieldCollapsed(field: ResponseFacetField) { - field.expanded = false; - } - - onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) { - const facetQuery = this.queryBuilder.getFacetQuery(query.label); - - if (event.checked) { - query.$checked = true; - this.selectedFacetQueries.push(facetQuery.label); - - if (facetQuery) { - this.queryBuilder.addFilterQuery(facetQuery.query); - } - } else { - query.$checked = false; - this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== query.label); - - if (facetQuery) { - this.queryBuilder.removeFilterQuery(facetQuery.query); + onToggleFacetQuery(event: MatCheckboxChange, facetQuery: FacetQuery) { + if (event && facetQuery) { + if (event.checked) { + this.selectFacetQuery(facetQuery); + } else { + this.unselectFacetQuery(facetQuery); } } - - this.queryBuilder.update(); } - onFacetToggle(event: MatCheckboxChange, field: ResponseFacetField, bucket: FacetFieldBucket) { - if (event.checked) { - bucket.$checked = true; - this.selectedBuckets.push({ ...bucket }); - this.queryBuilder.addFilterQuery(bucket.filterQuery); - } else { - bucket.$checked = false; - const idx = this.selectedBuckets.findIndex( - b => b.$field === bucket.$field && b.label === bucket.label - ); - - if (idx >= 0) { - this.selectedBuckets.splice(idx, 1); - } - this.queryBuilder.removeFilterQuery(bucket.filterQuery); - } - - this.queryBuilder.update(); - } - - unselectFacetQuery(label: string, reloadQuery: boolean = true) { - const facetQuery = this.queryBuilder.getFacetQuery(label); - if (facetQuery) { - this.queryBuilder.removeFilterQuery(facetQuery.query); - } - - this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== label); - - if (reloadQuery) { + selectFacetQuery(query: FacetQuery) { + if (query) { + query.checked = true; + this.queryBuilder.addUserFacetQuery(query); + this.updateSelectedFields(); this.queryBuilder.update(); } } - unselectFacetBucket(bucket: FacetFieldBucket, reloadQuery: boolean = true) { - if (bucket) { - const idx = this.selectedBuckets.findIndex( - selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label - ); + unselectFacetQuery(query: FacetQuery) { + if (query) { + query.checked = false; + this.queryBuilder.removeUserFacetQuery(query); + this.updateSelectedFields(); + this.queryBuilder.update(); + } + } - if (idx >= 0) { - this.selectedBuckets.splice(idx, 1); + private updateSelectedBuckets() { + if (this.responseFacetFields) { + this.selectedBuckets = []; + for (let field of this.responseFacetFields) { + if (field.buckets) { + this.selectedBuckets.push(...field.buckets.items.filter(bucket => bucket.checked)); + } } - this.queryBuilder.removeFilterQuery(bucket.filterQuery); + } else { + this.selectedBuckets = []; + } + } - bucket.$checked = false; + private updateSelectedFields() { + if (this.responseFacetQueries) { + this.selectedFacetQueries = this.responseFacetQueries.items.filter(item => item.checked); + this.canResetSelectedQueries = this.selectedFacetQueries.length > 0; + } else { + this.selectedFacetQueries = []; + this.canResetSelectedQueries = false; + } + } - if (reloadQuery) { - this.queryBuilder.update(); + onToggleBucket(event: MatCheckboxChange, bucket: FacetFieldBucket) { + if (event && bucket) { + if (event.checked) { + this.selectFacetBucket(bucket); + } else { + this.unselectFacetBucket(bucket); } } } - canResetSelectedQueries(): boolean { - return this.selectedFacetQueries && this.selectedFacetQueries.length > 0; + selectFacetBucket(bucket: FacetFieldBucket) { + if (bucket) { + bucket.checked = true; + this.queryBuilder.addUserFacetBucket(bucket); + this.updateSelectedBuckets(); + this.queryBuilder.update(); + } + } + + unselectFacetBucket(bucket: FacetFieldBucket) { + if (bucket) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(bucket); + this.updateSelectedBuckets(); + this.queryBuilder.update(); + } } resetSelectedQueries() { - if (this.canResetSelectedQueries()) { - this.selectedFacetQueries.forEach(query => { - this.unselectFacetQuery(query, false); - }); + if (this.canResetSelectedQueries) { + for (let query of this.responseFacetQueries.items) { + query.checked = false; + this.queryBuilder.removeUserFacetQuery(query); + } + this.selectedFacetQueries = []; + this.canResetSelectedQueries = false; this.queryBuilder.update(); } } - canResetSelectedBuckets(field: ResponseFacetField): boolean { + canResetSelectedBuckets(field: FacetField): boolean { if (field && field.buckets) { - return field.buckets.items.some(bucket => bucket.$checked); + return field.buckets.items.some(bucket => bucket.checked); } return false; } - resetSelectedBuckets(field: ResponseFacetField) { + resetSelectedBuckets(field: FacetField) { if (field && field.buckets) { - field.buckets.items.forEach(bucket => { - this.unselectFacetBucket(bucket, false); - }); + for (let bucket of field.buckets.items) { + bucket.checked = false; + this.queryBuilder.removeUserFacetBucket(bucket); + } + this.updateSelectedBuckets(); this.queryBuilder.update(); } } @@ -207,64 +192,75 @@ export class SearchFilterComponent implements OnInit, OnDestroy { const context = data.list.context; if (context) { - 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(field => field.expanded) - .map(field => field.label); - - this.responseFacetFields = (context.facetsFields || []).map( - field => { - const settings = this.queryBuilder.getFacetField(field.label); - - let fallbackPageSize = this.DEFAULT_PAGE_SIZE; - if (settings && settings.pageSize) { - fallbackPageSize = settings.pageSize; - } - - field.label = this.translationService.instant(field.label); - field.pageSize = field.pageSize || fallbackPageSize; - field.currentPageSize = field.pageSize; - field.expanded = expandedFields.includes(field.label); - - 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( - selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label - ); - if (previousBucket) { - bucket.$checked = true; - } - return bucket; - }); - - 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; - } - ); + this.parseFacetFields(context); + this.parseFacetQueries(context); } else { - this.responseFacetQueries = new ResponseFacetQueryList([], this.facetQueriesPageSize); - this.responseFacetFields = []; + this.responseFacetQueries = null; + this.responseFacetFields = null; } } + + private parseFacetFields(context: any) { + if (!this.responseFacetFields) { + const configFacetFields = this.queryBuilder.config.facetFields || []; + this.responseFacetFields = configFacetFields.map(field => { + const responseField = (context.facetsFields || []).find(response => response.label === field.label); + const buckets: FacetFieldBucket[] = (responseField.buckets || []).map(bucket => { + return { + ...bucket, + checked: false, + display: this.translationService.instant(bucket.display), + label: this.translationService.instant(bucket.label) + }; + }); + 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; + }; + return { + ...field, + label: this.translationService.instant(field.label), + pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE, + currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE, + buckets: bucketList + }; + }); + } + } + + private parseFacetQueries(context: any) { + const responseQueries = this.getFacetQueryMap(context); + if (!this.responseFacetQueries) { + const facetQueries = (this.queryBuilder.config.facetQueries.queries || []) + .map(query => { + const queryResult = responseQueries[query.label]; + return { + ...query, + label: this.translationService.instant(query.label), + count: queryResult.count + }; + }); + + if (facetQueries.length > 0) { + this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize); + } else { + this.responseFacetQueries = null; + } + } + } + + private getFacetQueryMap(context: any): { [key: string]: any } { + const result = {}; + + (context.facetQueries || []).forEach(query => { + result[query.label] = query; + }); + + return result; + } } diff --git a/lib/content-services/search/facet-field-bucket.interface.ts b/lib/content-services/search/facet-field-bucket.interface.ts index 47bcab9fcd..42623602e4 100644 --- a/lib/content-services/search/facet-field-bucket.interface.ts +++ b/lib/content-services/search/facet-field-bucket.interface.ts @@ -21,6 +21,5 @@ export interface FacetFieldBucket { label: string; filterQuery: string; - $checked?: boolean; - $field?: string; + checked?: boolean; } diff --git a/lib/content-services/search/facet-field.interface.ts b/lib/content-services/search/facet-field.interface.ts index 96f19b11b5..cc50641bc6 100644 --- a/lib/content-services/search/facet-field.interface.ts +++ b/lib/content-services/search/facet-field.interface.ts @@ -15,6 +15,9 @@ * limitations under the License. */ +import { SearchFilterList } from './components/search-filter/models/search-filter-list.model'; +import { FacetFieldBucket } from './facet-field-bucket.interface'; + export interface FacetField { field: string; label: string; @@ -23,6 +26,8 @@ export interface FacetField { offset?: number; prefix?: string; + buckets?: SearchFilterList; pageSize?: number; - $checked?: boolean; + currentPageSize?: number; + checked?: boolean; } diff --git a/lib/content-services/search/facet-query.interface.ts b/lib/content-services/search/facet-query.interface.ts index e810f2d4d3..9b7a15210f 100644 --- a/lib/content-services/search/facet-query.interface.ts +++ b/lib/content-services/search/facet-query.interface.ts @@ -16,12 +16,9 @@ */ export interface FacetQuery { - query: string; label: string; -} + query: string; -export interface ResponseFacetQuery { - label?: string; - filterQuery?: string; + checked?: boolean; count?: number; } diff --git a/lib/content-services/search/public-api.ts b/lib/content-services/search/public-api.ts index df82cba669..5decc42ae7 100644 --- a/lib/content-services/search/public-api.ts +++ b/lib/content-services/search/public-api.ts @@ -19,8 +19,6 @@ export { FacetFieldBucket } from './facet-field-bucket.interface'; export { FacetField } from './facet-field.interface'; export { FacetQuery } from './facet-query.interface'; export { FilterQuery } from './filter-query.interface'; -export { ResponseFacetField } from './response-facet-field.interface'; -export { ResponseFacetQuery } from './response-facet-query.interface'; export { SearchCategory } from './search-category.interface'; export { SearchWidgetSettings } from './search-widget-settings.interface'; export { SearchWidget } from './search-widget.interface'; diff --git a/lib/content-services/search/response-facet-field.interface.ts b/lib/content-services/search/response-facet-field.interface.ts deleted file mode 100644 index dfb973daa6..0000000000 --- a/lib/content-services/search/response-facet-field.interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! - * @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 { FacetFieldBucket } from './facet-field-bucket.interface'; -import { SearchFilterList } from './components/search-filter/models/search-filter-list.model'; - -export interface ResponseFacetField { - label: string; - buckets: SearchFilterList; - pageSize?: number; - currentPageSize?: number; - expanded?: boolean; -} diff --git a/lib/content-services/search/response-facet-query.interface.ts b/lib/content-services/search/response-facet-query.interface.ts deleted file mode 100644 index 2a6df4149f..0000000000 --- a/lib/content-services/search/response-facet-query.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * @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. - */ - -export interface ResponseFacetQuery { - label: string; - mincount: number; - - $checked?: boolean; -} diff --git a/lib/content-services/search/search-query-builder.service.spec.ts b/lib/content-services/search/search-query-builder.service.spec.ts index 40fb0964f7..3d27c8c691 100644 --- a/lib/content-services/search/search-query-builder.service.spec.ts +++ b/lib/content-services/search/search-query-builder.service.spec.ts @@ -212,7 +212,7 @@ describe('SearchQueryBuilder', () => { const builder = new SearchQueryBuilderService(buildConfig(config), null); const field = builder.getFacetField('Missing'); - expect(field).toBeUndefined(); + expect(field).toBeFalsy(); }); xit('should build query and raise an event on update', async () => { diff --git a/lib/content-services/search/search-query-builder.service.ts b/lib/content-services/search/search-query-builder.service.ts index fb798bc1c2..ce5b110532 100644 --- a/lib/content-services/search/search-query-builder.service.ts +++ b/lib/content-services/search/search-query-builder.service.ts @@ -26,6 +26,7 @@ import { SearchConfiguration } from './search-configuration.interface'; import { FacetQuery } from './facet-query.interface'; import { SearchSortingDefinition } from './search-sorting-definition.interface'; import { FacetField } from './facet-field.interface'; +import { FacetFieldBucket } from './facet-field-bucket.interface'; @Injectable() export class SearchQueryBuilderService { @@ -41,6 +42,9 @@ export class SearchQueryBuilderService { paging: { maxItems?: number; skipCount?: number } = null; sorting: Array = []; + protected userFacetQueries: FacetQuery[] = []; + protected userFacetBuckets: FacetFieldBucket[] = []; + get userQuery(): string { return this._userQuery; } @@ -65,12 +69,48 @@ export class SearchQueryBuilderService { this.config = JSON.parse(JSON.stringify(template)); this.categories = (this.config.categories || []).filter(category => category.enabled); this.filterQueries = this.config.filterQueries || []; + this.userFacetBuckets = []; + this.userFacetQueries = []; if (this.config.sorting) { this.sorting = this.config.sorting.defaults || []; } } } + addUserFacetQuery(query: FacetQuery) { + if (query) { + const existing = this.userFacetQueries.find(facetQuery => facetQuery.label === query.label); + if (existing) { + existing.query = query.query; + } else { + this.userFacetQueries.push({ ...query }); + } + } + } + + removeUserFacetQuery(query: FacetQuery) { + if (query) { + this.userFacetQueries = this.userFacetQueries + .filter(facetQuery => facetQuery.label !== query.label); + } + } + + addUserFacetBucket(bucket: FacetFieldBucket) { + if (bucket) { + const existing = this.userFacetBuckets.find(facetBucket => facetBucket.label === bucket.label); + if (!existing) { + this.userFacetBuckets.push(bucket); + } + } + } + + removeUserFacetBucket(bucket: FacetFieldBucket) { + if (bucket) { + this.userFacetBuckets = this.userFacetBuckets + .filter(facetBucket => facetBucket.label !== bucket.label); + } + } + addFilterQuery(query: string): void { if (query) { const existing = this.filterQueries.find(filterQuery => filterQuery.query === query); @@ -89,7 +129,10 @@ export class SearchQueryBuilderService { getFacetQuery(label: string): FacetQuery { if (label && this.hasFacetQueries) { - return this.config.facetQueries.queries.find(query => query.label === label); + const result = this.config.facetQueries.queries.find(query => query.label === label); + if (result) { + return { ...result }; + } } return null; } @@ -97,7 +140,10 @@ export class SearchQueryBuilderService { getFacetField(label: string): FacetField { if (label) { const fields = this.config.facetFields || []; - return fields.find(field => field.label === label); + const result = fields.find(field => field.label === label); + if (result) { + return { ...result }; + } } return null; } @@ -175,7 +221,7 @@ export class SearchQueryBuilderService { return false; } - private get sort(): RequestSortDefinitionInner[] { + protected get sort(): RequestSortDefinitionInner[] { return this.sorting.map(def => { return { type: def.type, @@ -185,7 +231,7 @@ export class SearchQueryBuilderService { }); } - private get facetQueries(): FacetQuery[] { + protected get facetQueries(): FacetQuery[] { if (this.hasFacetQueries) { return this.config.facetQueries.queries.map(query => { return { ...query }; @@ -195,7 +241,7 @@ export class SearchQueryBuilderService { return null; } - private getFinalQuery(): string { + protected getFinalQuery(): string { let query = ''; this.categories.forEach(facet => { @@ -208,14 +254,28 @@ export class SearchQueryBuilderService { } }); - const result = [this.userQuery, query] + let result = [this.userQuery, query] .filter(entry => entry) .join(' AND '); + if (this.userFacetQueries && this.userFacetQueries.length > 0) { + const combined = this.userFacetQueries + .map(userQuery => userQuery.query) + .join(' OR '); + result += ` AND (${combined})`; + } + + if (this.userFacetBuckets && this.userFacetBuckets.length > 0) { + const combined = this.userFacetBuckets + .map(bucket => bucket.filterQuery) + .join(' OR '); + result += ` AND (${combined})`; + } + return result; } - private get facetFields(): RequestFacetFields { + protected get facetFields(): RequestFacetFields { const facetFields = this.config.facetFields; if (facetFields && facetFields.length > 0) { diff --git a/tools/export-check/export-2.3.0.json b/tools/export-check/export-2.3.0.json index 3fcc927f85..14aef59f2a 100644 --- a/tools/export-check/export-2.3.0.json +++ b/tools/export-check/export-2.3.0.json @@ -5031,22 +5031,6 @@ }, "name": "RequiredFieldValidator" }, - { - "position": { - "line": 21, - "character": 9, - "fileName": "lib/content-services/search/public-api.ts" - }, - "name": "ResponseFacetField" - }, - { - "position": { - "line": 22, - "character": 9, - "fileName": "lib/content-services/search/public-api.ts" - }, - "name": "ResponseFacetQuery" - }, { "position": { "line": 25,