diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index 2d3ea63b49..a20d590726 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -65,16 +65,19 @@ { "field": "modifier", "mincount": 1, "label": "Modifier" }, { "field": "created", "mincount": 1, "label": "Created" } ], - "facetQueries": [ - { "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" } - ], + "facetQueries": { + "label": "My facet queries", + "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" } + ] + }, "categories": [ { "id": "queryName", diff --git a/docs/content-services/search-filter.component.md b/docs/content-services/search-filter.component.md index 427f7112be..ca8797d30a 100644 --- a/docs/content-services/search-filter.component.md +++ b/docs/content-services/search-filter.component.md @@ -19,6 +19,12 @@ Represents a main container component for custom search and faceted search setti The component is based on dynamically created widgets to modify the resulting query and options, and the [Search Query Builder service](search-query-builder.service.md)\` to build and execute the search queries. +Before you begin with customizations, check also the following articles: + +- [Search API](https://docs.alfresco.com/5.2/concepts/search-api.html) +- [Alfresco Full Text Search Reference](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-intro.html) +- [ACS API Explorer](https://api-explorer.alfresco.com/api-explorer/#!/search/search) + ### Configuration The configuration should be provided via the `search` entry in the `app.config.json` file. @@ -121,6 +127,9 @@ The interface above also describes entries in the `search.query.categories` sect ![Search Categories](../docassets/images/search-categories-01.png) +Important note: you need at least one category field to be provided in order to execute the query, +so that filters and selected facets are applied. + ### Settings Every use case will have a different set of settings. @@ -161,24 +170,35 @@ If there are more than 5 entries, the "Show more" button is displayed to allow d ### Facet Queries +Provides a custom category based on admin-defined facet queries. + ```json { "search": { - "facetQueries": [ - { "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" } - ] + "facetQueries": { + "label": "Facet queries", + "pageSize": 5, + "expanded": true, + "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" } + ] + } } } ``` The queries declared in the `facetQueries` are collected into a single collapsible category. +Only the queries that have 1 or more response entries are displayed at runtime. +Based on the `pageSize` value, the component provides a `Show more` button to display more items. + +You can also provide a custom `label` (or i18n resource key) for the resulting collapsible category. ![Facet Queries](../docassets/images/search-facet-queries.png) @@ -350,6 +370,13 @@ Provides ability to select a numeric range based on `min` and `max` values in th ![Slider Widget](../docassets/images/search-slider.png) +### Resetting slider value + +Slider widget comes with a `Clear` button that allows users to reset selected value to the initial state. + +This helps to undo changes for scenarios where minimal value (like 0 or predefined number) still should not be used in a query. +Upon clicking the `Clear` button slider will be reset to the `min` value or `0`, and underlying fragment is removed from the resulting query. + ### Text Widget ```json @@ -377,6 +404,9 @@ Provides ability to select a numeric range based on `min` and `max` values in th ![Text Widget](../docassets/images/search-text.png) +Important note: you need at least one category field to be provided in order to execute the query, +so that filters and selected facets are applied. + ## Custom Widgets ### Implementing custom widget diff --git a/lib/content-services/i18n/en.json b/lib/content-services/i18n/en.json index b9e4c0eb53..4cd29e1d0c 100644 --- a/lib/content-services/i18n/en.json +++ b/lib/content-services/i18n/en.json @@ -174,7 +174,8 @@ "ACTIONS": { "CLEAR": "Clear", "APPLY": "Apply", - "CLEAR-ALL": "Clear all" + "CLEAR-ALL": "Clear all", + "SHOW-MORE": "Show more" }, "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 new file mode 100644 index 0000000000..d19fd0d8af --- /dev/null +++ b/lib/content-services/search/components/search-filter/models/response-facet-query-list.model.ts @@ -0,0 +1,58 @@ +/*! + * @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 { ResponseFacetQuery } from '../../../facet-query.interface'; + +export class ResponseFacetQueryList { + + items: ResponseFacetQuery[] = []; + pageSize: number = 5; + currentPageSize: number = 5; + + get visibleItems(): ResponseFacetQuery[] { + return this.items.slice(0, this.currentPageSize); + } + + get length(): number { + return this.items.length; + } + + constructor(items: ResponseFacetQuery[] = [], pageSize: number = 5) { + this.items = items + .filter(item => { + return item.count > 0; + }) + .map(item => { + return { ...item }; + }); + this.pageSize = pageSize; + this.currentPageSize = pageSize; + } + + hasMoreItems(): boolean { + return this.items.length > this.currentPageSize; + } + + showMoreItems() { + this.currentPageSize += this.pageSize; + } + + clear() { + this.currentPageSize = this.pageSize; + this.items = []; + } +} 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 957ef03bb9..99760e6d6c 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 @@ -17,20 +17,25 @@ - + - Facet Queries + {{ facetQueriesLabel | translate }}
- + {{ query.label }} ({{ query.count }})
+
- Show more + {{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }} keyboard_arrow_down 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 f4085e4626..c1649c3db6 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 @@ -20,6 +20,7 @@ import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchConfiguration } from '../../search-configuration.interface'; import { AppConfigService } from '@alfresco/adf-core'; import { Subject } from 'rxjs/Subject'; +import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; describe('SearchSettingsComponent', () => { @@ -119,9 +120,11 @@ describe('SearchSettingsComponent', () => { it('should unselect facet query and update builder', () => { const config: SearchConfiguration = { categories: [], - facetQueries: [ - { label: 'q1', query: 'query1' } - ] + facetQueries: { + queries: [ + { label: 'q1', query: 'query1' } + ] + } }; appConfig.config.search = config; queryBuilder = new SearchQueryBuilderService(appConfig, null); @@ -155,10 +158,10 @@ describe('SearchSettingsComponent', () => { }); it('should fetch facet queries from response payload', () => { - component.responseFacetQueries = []; + component.responseFacetQueries = new ResponseFacetQueryList(); const queries = [ - { label: 'q1', query: 'query1' }, - { label: 'q2', query: 'query2' } + { label: 'q1', query: 'query1', count: 1 }, + { label: 'q2', query: 'query2', count: 1 } ]; const data = { list: { @@ -171,11 +174,11 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); expect(component.responseFacetQueries.length).toBe(2); - expect(component.responseFacetQueries).toEqual(queries); + expect(component.responseFacetQueries.items).toEqual(queries); }); it('should not fetch facet queries from response payload', () => { - component.responseFacetQueries = []; + component.responseFacetQueries = new ResponseFacetQueryList(); const data = { list: { @@ -192,11 +195,11 @@ describe('SearchSettingsComponent', () => { it('should restore checked state for new response facet queries', () => { component.selectedFacetQueries = ['q3']; - component.responseFacetQueries = []; + component.responseFacetQueries = new ResponseFacetQueryList(); const queries = [ - { label: 'q1', query: 'query1' }, - { label: 'q2', query: 'query2' } + { label: 'q1', query: 'query1', count: 1 }, + { label: 'q2', query: 'query2', count: 1 } ]; const data = { list: { @@ -209,17 +212,17 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); expect(component.responseFacetQueries.length).toBe(2); - expect(( component.responseFacetQueries[0]).$checked).toBeFalsy(); - expect(( component.responseFacetQueries[1]).$checked).toBeFalsy(); + 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 = []; + component.responseFacetQueries = new ResponseFacetQueryList(); const queries = [ - { label: 'q1', query: 'query1' }, - { label: 'q2', query: 'query2' } + { label: 'q1', query: 'query1', count: 1 }, + { label: 'q2', query: 'query2', count: 1 } ]; const data = { list: { @@ -232,8 +235,8 @@ describe('SearchSettingsComponent', () => { component.onDataLoaded(data); expect(component.responseFacetQueries.length).toBe(2); - expect(( component.responseFacetQueries[0]).$checked).toBeFalsy(); - expect(( component.responseFacetQueries[1]).$checked).toBeTruthy(); + expect(( component.responseFacetQueries.items[0]).$checked).toBeFalsy(); + expect(( component.responseFacetQueries.items[1]).$checked).toBeTruthy(); }); it('should fetch facet fields from response payload', () => { @@ -309,7 +312,7 @@ describe('SearchSettingsComponent', () => { }); it('should reset queries and fields on empty response payload', () => { - component.responseFacetQueries = [ {}, {}]; + component.responseFacetQueries = new ResponseFacetQueryList([ {}, {}]); component.responseFacetFields = [ {}, {}]; const data = { 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 54d5030eb5..205d372e86 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,11 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material'; import { SearchService } from '@alfresco/adf-core'; import { SearchQueryBuilderService } from '../../search-query-builder.service'; -import { FacetQuery } from '../../facet-query.interface'; 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'; @Component({ selector: 'adf-search-filter', @@ -36,10 +36,20 @@ export class SearchFilterComponent implements OnInit { selectedFacetQueries: string[] = []; selectedBuckets: FacetFieldBucket[] = []; - responseFacetQueries: FacetQuery[] = []; + responseFacetQueries: ResponseFacetQueryList; responseFacetFields: ResponseFacetField[] = []; + facetQueriesLabel: string = 'Facet Queries'; + facetQueriesPageSize = 5; + facetQueriesExpanded = false; + constructor(public queryBuilder: SearchQueryBuilderService, private search: SearchService) { + if (queryBuilder.config && queryBuilder.config.facetQueries) { + this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries'; + this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || 5; + this.facetQueriesExpanded = queryBuilder.config.facetQueries.expanded; + } + this.queryBuilder.updated.subscribe(query => { this.queryBuilder.execute(); }); @@ -138,11 +148,13 @@ export class SearchFilterComponent implements OnInit { const context = data.list.context; if (context) { - this.responseFacetQueries = (context.facetQueries || []).map(q => { + const facetQueries = (context.facetQueries || []).map(q => { q.$checked = this.selectedFacetQueries.includes(q.label); return q; }); + this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize); + const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label); this.responseFacetFields = (context.facetsFields || []).map( @@ -180,7 +192,7 @@ export class SearchFilterComponent implements OnInit { } ); } else { - this.responseFacetQueries = []; + this.responseFacetQueries = new ResponseFacetQueryList([], this.facetQueriesPageSize); this.responseFacetFields = []; } } diff --git a/lib/content-services/search/components/search-slider/search-slider.component.html b/lib/content-services/search/components/search-slider/search-slider.component.html index adb5aecf52..3b177f3e13 100644 --- a/lib/content-services/search/components/search-slider/search-slider.component.html +++ b/lib/content-services/search/components/search-slider/search-slider.component.html @@ -1,8 +1,15 @@ + +
+ +
diff --git a/lib/content-services/search/components/search-slider/search-slider.component.spec.ts b/lib/content-services/search/components/search-slider/search-slider.component.spec.ts index cdad747c2c..d669685349 100644 --- a/lib/content-services/search/components/search-slider/search-slider.component.spec.ts +++ b/lib/content-services/search/components/search-slider/search-slider.component.spec.ts @@ -70,4 +70,62 @@ describe('SearchSliderComponent', () => { expect(context.update).toHaveBeenCalled(); }); + it('should reset the value for query builder', () => { + const settings: any = { + field: 'field1', + min: 10, + max: 100, + step: 2, + thumbLabel: true + }; + + const context: any = { + queryFragments: {}, + update() {} + }; + + component.settings = settings; + component.context = context; + component.value = 20; + component.id = 'slider'; + component.ngOnInit(); + + spyOn(context, 'update').and.stub(); + + component.reset(); + + expect(component.value).toBe(settings.min); + expect(context.queryFragments['slider']).toBe(''); + expect(context.update).toHaveBeenCalled(); + }); + + it('should reset to 0 if min not provided', () => { + const settings: any = { + field: 'field1', + min: null, + max: 100, + step: 2, + thumbLabel: true + }; + + const context: any = { + queryFragments: {}, + update() {} + }; + + component.settings = settings; + component.context = context; + component.value = 20; + component.id = 'slider'; + component.ngOnInit(); + + spyOn(context, 'update').and.stub(); + + component.reset(); + + expect(component.value).toBe(0); + expect(context.queryFragments['slider']).toBe(''); + expect(context.update).toHaveBeenCalled(); + }); + }); diff --git a/lib/content-services/search/components/search-slider/search-slider.component.ts b/lib/content-services/search/components/search-slider/search-slider.component.ts index bbe29b4e0e..5b1d72a668 100644 --- a/lib/content-services/search/components/search-slider/search-slider.component.ts +++ b/lib/content-services/search/components/search-slider/search-slider.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, ViewEncapsulation, OnInit } from '@angular/core'; +import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core'; import { SearchWidget } from '../../search-widget.interface'; import { SearchWidgetSettings } from '../../search-widget-settings.interface'; import { SearchQueryBuilderService } from '../../search-query-builder.service'; @@ -37,7 +37,9 @@ export class SearchSliderComponent implements SearchWidget, OnInit { min: number; max: number; thumbLabel = false; - value: number; + + @Input() + value: number | null; ngOnInit() { if (this.settings) { @@ -57,11 +59,23 @@ export class SearchSliderComponent implements SearchWidget, OnInit { } } + reset() { + this.value = this.min || 0; + this.updateQuery(null); + } + onChangedHandler(event: MatSliderChange) { this.value = event.value; + this.updateQuery(this.value); + } + private updateQuery(value: number | null) { if (this.id && this.context && this.settings && this.settings.field) { - this.context.queryFragments[this.id] = `${this.settings.field}:[0 TO ${this.value}]`; + if (value === null) { + this.context.queryFragments[this.id] = ''; + } else { + this.context.queryFragments[this.id] = `${this.settings.field}:[0 TO ${value}]`; + } this.context.update(); } } diff --git a/lib/content-services/search/components/search-text/search-text.component.html b/lib/content-services/search/components/search-text/search-text.component.html index 4b4c1049c2..b05b9e811a 100644 --- a/lib/content-services/search/components/search-text/search-text.component.html +++ b/lib/content-services/search/components/search-text/search-text.component.html @@ -2,6 +2,9 @@ + diff --git a/lib/content-services/search/components/search-text/search-text.component.scss b/lib/content-services/search/components/search-text/search-text.component.scss new file mode 100644 index 0000000000..62a8a84b85 --- /dev/null +++ b/lib/content-services/search/components/search-text/search-text.component.scss @@ -0,0 +1,5 @@ +.adf-search-text { + .mat-input-container { + width: 100% + } +} diff --git a/lib/content-services/search/components/search-text/search-text.component.spec.ts b/lib/content-services/search/components/search-text/search-text.component.spec.ts new file mode 100644 index 0000000000..f38a3f8b31 --- /dev/null +++ b/lib/content-services/search/components/search-text/search-text.component.spec.ts @@ -0,0 +1,88 @@ +/*! + * @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 { SearchTextComponent } from './search-text.component'; + +describe('SearchTextComponent', () => { + + let component: SearchTextComponent; + + beforeEach(() => { + component = new SearchTextComponent(); + component.id = 'text'; + component.settings = { + 'pattern': "cm:name:'(.*?)'", + 'field': 'cm:name', + 'placeholder': 'Enter the name' + }; + + component.context = { + queryFragments: {}, + update() {} + }; + }); + + it('should parse value from the context at startup', () => { + component.context.queryFragments[component.id] = "cm:name:'secret.pdf'"; + component.ngOnInit(); + + expect(component.value).toEqual('secret.pdf'); + }); + + it('should not parse value when pattern not defined', () => { + component.settings.pattern = null; + component.context.queryFragments[component.id] = "cm:name:'secret.pdf'"; + component.ngOnInit(); + + expect(component.value).toEqual(''); + }); + + it('should update query builder on change', () => { + spyOn(component.context, 'update').and.stub(); + + component.onChangedHandler({ + target: { + value: 'top-secret.doc' + } + }); + + expect(component.value).toBe('top-secret.doc'); + expect(component.context.queryFragments[component.id]).toBe("cm:name:'top-secret.doc'"); + expect(component.context.update).toHaveBeenCalled(); + }); + + it('should reset query builder', () => { + component.onChangedHandler({ + target: { + value: 'top-secret.doc' + } + }); + + expect(component.value).toBe('top-secret.doc'); + expect(component.context.queryFragments[component.id]).toBe("cm:name:'top-secret.doc'"); + + component.onChangedHandler({ + target: { + value: '' + } + }); + + expect(component.value).toBe(''); + expect(component.context.queryFragments[component.id]).toBe(''); + }); + +}); diff --git a/lib/content-services/search/components/search-text/search-text.component.ts b/lib/content-services/search/components/search-text/search-text.component.ts index 985928a8fa..fcee4e5114 100644 --- a/lib/content-services/search/components/search-text/search-text.component.ts +++ b/lib/content-services/search/components/search-text/search-text.component.ts @@ -23,6 +23,7 @@ import { SearchQueryBuilderService } from '../../search-query-builder.service'; @Component({ selector: 'adf-search-text', templateUrl: './search-text.component.html', + styleUrls: ['./search-text.component.scss'], encapsulation: ViewEncapsulation.None, host: { class: 'adf-search-text' } }) @@ -36,7 +37,7 @@ export class SearchTextComponent implements SearchWidget, OnInit { context: SearchQueryBuilderService; ngOnInit() { - if (this.context && this.settings) { + if (this.context && this.settings && this.settings.pattern) { const pattern = new RegExp(this.settings.pattern, 'g'); const match = pattern.exec(this.context.queryFragments[this.id] || ''); @@ -46,10 +47,19 @@ export class SearchTextComponent implements SearchWidget, OnInit { } } + reset() { + this.value = ''; + this.updateQuery(null); + } + onChangedHandler(event) { this.value = event.target.value; - if (this.value) { - this.context.queryFragments[this.id] = `${this.settings.field}:'${this.value}'`; + this.updateQuery(this.value); + } + + private updateQuery(value: string) { + if (this.context && this.settings && this.settings.field) { + this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${value}'` : ''; this.context.update(); } } diff --git a/lib/content-services/search/facet-query.interface.ts b/lib/content-services/search/facet-query.interface.ts index 3a83586447..e810f2d4d3 100644 --- a/lib/content-services/search/facet-query.interface.ts +++ b/lib/content-services/search/facet-query.interface.ts @@ -19,3 +19,9 @@ export interface FacetQuery { query: string; label: string; } + +export interface ResponseFacetQuery { + label?: string; + filterQuery?: string; + count?: number; +} diff --git a/lib/content-services/search/search-configuration.interface.ts b/lib/content-services/search/search-configuration.interface.ts index f948453921..56a0a136c6 100644 --- a/lib/content-services/search/search-configuration.interface.ts +++ b/lib/content-services/search/search-configuration.interface.ts @@ -25,6 +25,11 @@ export interface SearchConfiguration { fields?: Array; categories: Array; filterQueries?: Array; - facetQueries?: Array; + facetQueries?: { + label?: string; + pageSize?: number; + expanded?: boolean; + queries: Array; + }; facetFields?: Array; } 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 22884d64b7..63f24f8087 100644 --- a/lib/content-services/search/search-query-builder.service.spec.ts +++ b/lib/content-services/search/search-query-builder.service.spec.ts @@ -120,10 +120,12 @@ describe('SearchQueryBuilder', () => { it('should fetch facet query from config', () => { const config: SearchConfiguration = { categories: [], - facetQueries: [ - { query: 'q1', label: 'query1' }, - { query: 'q2', label: 'query2' } - ] + facetQueries: { + queries: [ + { query: 'q1', label: 'query1' }, + { query: 'q2', label: 'query2' } + ] + } }; const builder = new SearchQueryBuilderService(buildConfig(config), null); const query = builder.getFacetQuery('query2'); @@ -135,9 +137,11 @@ describe('SearchQueryBuilder', () => { it('should not fetch empty facet query from the config', () => { const config: SearchConfiguration = { categories: [], - facetQueries: [ - { query: 'q1', label: 'query1' } - ] + facetQueries: { + queries: [ + { query: 'q1', label: 'query1' } + ] + } }; const builder = new SearchQueryBuilderService(buildConfig(config), null); @@ -273,15 +277,17 @@ describe('SearchQueryBuilder', () => { categories: [ { id: 'cat1', enabled: true } ], - facetQueries: [ - { query: 'q1', label: 'q2' } - ] + facetQueries: { + queries: [ + { query: 'q1', label: 'q2' } + ] + } }; const builder = new SearchQueryBuilderService(buildConfig(config), null); builder.queryFragments['cat1'] = 'cm:name:test'; const compiled = builder.buildQuery(); - expect(compiled.facetQueries).toEqual(config.facetQueries); + expect(compiled.facetQueries).toEqual(config.facetQueries.queries); }); it('should build query with custom facet fields', () => { diff --git a/lib/content-services/search/search-query-builder.service.ts b/lib/content-services/search/search-query-builder.service.ts index 5cc7dd2369..ece7e21fd4 100644 --- a/lib/content-services/search/search-query-builder.service.ts +++ b/lib/content-services/search/search-query-builder.service.ts @@ -69,7 +69,7 @@ export class SearchQueryBuilderService { getFacetQuery(label: string): FacetQuery { if (label) { - const queries = this.config.facetQueries || []; + const queries = this.config.facetQueries.queries || []; return queries.find(q => q.label === label); } return null; @@ -115,7 +115,7 @@ export class SearchQueryBuilderService { paging: this.paging, fields: this.config.fields, filterQueries: this.filterQueries, - facetQueries: this.config.facetQueries, + facetQueries: this.facetQueries, facetFields: this.facetFields }; @@ -125,6 +125,18 @@ export class SearchQueryBuilderService { return null; } + private get facetQueries(): FacetQuery[] { + const config = this.config.facetQueries; + + if (config && config.queries && config.queries.length > 0) { + return config.queries.map(query => { + return { ...query }; + }); + } + + return null; + } + private get facetFields(): RequestFacetFields { const facetFields = this.config.facetFields; diff --git a/lib/core/app-config/schema.json b/lib/core/app-config/schema.json index 6e77397b17..c2f25106c6 100644 --- a/lib/core/app-config/schema.json +++ b/lib/core/app-config/schema.json @@ -539,13 +539,32 @@ } }, "facetQueries": { - "type": "array", - "items": { - "type": "object", - "required": [ "query", "label" ], - "properties": { - "query": { "type": "string" }, - "label": { "type": "string" } + "type": "object", + "required": ["label", "queries"], + "properties": { + "label": { + "description": "Category label text", + "type": "string" + }, + "pageSize": { + "description": "Default page size of the category", + "type": "number" + }, + "expanded": { + "description": "Toggles expanded state of the category", + "type": "boolean" + }, + "queries": { + "description": "List of custom facet queries", + "type": "array", + "items": { + "type": "object", + "required": [ "query", "label" ], + "properties": { + "query": { "type": "string" }, + "label": { "type": "string" } + } + } } } },