search bug fixes and documentation updates (#3256)

* bug fixes for search

* test fixes

* bug fixes for search
This commit is contained in:
Denys Vuika
2018-05-03 10:28:20 +01:00
committed by Eugenio Romano
parent a9ab0af640
commit 856c4fd7f5
19 changed files with 423 additions and 78 deletions

View File

@@ -65,7 +65,9 @@
{ "field": "modifier", "mincount": 1, "label": "Modifier" }, { "field": "modifier", "mincount": 1, "label": "Modifier" },
{ "field": "created", "mincount": 1, "label": "Created" } { "field": "created", "mincount": 1, "label": "Created" }
], ],
"facetQueries": [ "facetQueries": {
"label": "My facet queries",
"queries": [
{ "query": "created:2018", "label": "Created This Year" }, { "query": "created:2018", "label": "Created This Year" },
{ "query": "content.mimetype", "label": "Type" }, { "query": "content.mimetype", "label": "Type" },
{ "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"}, { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"},
@@ -74,7 +76,8 @@
{ "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" }, { "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" },
{ "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
], ]
},
"categories": [ "categories": [
{ {
"id": "queryName", "id": "queryName",

View File

@@ -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, 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. 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 ### Configuration
The configuration should be provided via the `search` entry in the `app.config.json` file. 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) ![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 ### Settings
Every use case will have a different set of settings. Every use case will have a different set of settings.
@@ -161,10 +170,16 @@ If there are more than 5 entries, the "Show more" button is displayed to allow d
### Facet Queries ### Facet Queries
Provides a custom category based on admin-defined facet queries.
```json ```json
{ {
"search": { "search": {
"facetQueries": [ "facetQueries": {
"label": "Facet queries",
"pageSize": 5,
"expanded": true,
"queries": [
{ "query": "created:2018", "label": "Created This Year" }, { "query": "created:2018", "label": "Created This Year" },
{ "query": "content.mimetype", "label": "Type" }, { "query": "content.mimetype", "label": "Type" },
{ "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"}, { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"},
@@ -176,9 +191,14 @@ If there are more than 5 entries, the "Show more" button is displayed to allow d
] ]
} }
} }
}
``` ```
The queries declared in the `facetQueries` are collected into a single collapsible category. 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) ![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) ![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 ### Text Widget
```json ```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) ![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 ## Custom Widgets
### Implementing custom widget ### Implementing custom widget

View File

@@ -174,7 +174,8 @@
"ACTIONS": { "ACTIONS": {
"CLEAR": "Clear", "CLEAR": "Clear",
"APPLY": "Apply", "APPLY": "Apply",
"CLEAR-ALL": "Clear all" "CLEAR-ALL": "Clear all",
"SHOW-MORE": "Show more"
}, },
"RANGE": { "RANGE": {
"FROM": "From", "FROM": "From",

View File

@@ -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 <ResponseFacetQuery> { ...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 = [];
}
}

View File

@@ -17,20 +17,25 @@
</adf-search-widget-container> </adf-search-widget-container>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel> <mat-expansion-panel [expanded]="facetQueriesExpanded">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>Facet Queries</mat-panel-title> <mat-panel-title>{{ facetQueriesLabel | translate }}</mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="checklist"> <div class="checklist">
<ng-container *ngFor="let query of responseFacetQueries"> <ng-container *ngFor="let query of responseFacetQueries.visibleItems">
<mat-checkbox <mat-checkbox
*ngIf="query.count > 0"
[checked]="query.$checked" [checked]="query.$checked"
(change)="onFacetQueryToggle($event, query)"> (change)="onFacetQueryToggle($event, query)">
{{ query.label }} ({{ query.count }}) {{ query.label }} ({{ query.count }})
</mat-checkbox> </mat-checkbox>
</ng-container> </ng-container>
</div> </div>
<button mat-button
*ngIf="responseFacetQueries.hasMoreItems()"
(click)="responseFacetQueries.showMoreItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel <mat-expansion-panel
@@ -53,7 +58,7 @@
<button mat-button <button mat-button
*ngIf="field.hasMoreItems()" *ngIf="field.hasMoreItems()"
(click)="field.showMoreItems()"> (click)="field.showMoreItems()">
Show more {{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon> <mat-icon>keyboard_arrow_down</mat-icon>
</button> </button>
</mat-expansion-panel> </mat-expansion-panel>

View File

@@ -20,6 +20,7 @@ import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchConfiguration } from '../../search-configuration.interface'; import { SearchConfiguration } from '../../search-configuration.interface';
import { AppConfigService } from '@alfresco/adf-core'; import { AppConfigService } from '@alfresco/adf-core';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { ResponseFacetQueryList } from './models/response-facet-query-list.model';
describe('SearchSettingsComponent', () => { describe('SearchSettingsComponent', () => {
@@ -119,9 +120,11 @@ describe('SearchSettingsComponent', () => {
it('should unselect facet query and update builder', () => { it('should unselect facet query and update builder', () => {
const config: SearchConfiguration = { const config: SearchConfiguration = {
categories: [], categories: [],
facetQueries: [ facetQueries: {
queries: [
{ label: 'q1', query: 'query1' } { label: 'q1', query: 'query1' }
] ]
}
}; };
appConfig.config.search = config; appConfig.config.search = config;
queryBuilder = new SearchQueryBuilderService(appConfig, null); queryBuilder = new SearchQueryBuilderService(appConfig, null);
@@ -155,10 +158,10 @@ describe('SearchSettingsComponent', () => {
}); });
it('should fetch facet queries from response payload', () => { it('should fetch facet queries from response payload', () => {
component.responseFacetQueries = []; component.responseFacetQueries = new ResponseFacetQueryList();
const queries = [ const queries = [
{ label: 'q1', query: 'query1' }, { label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2' } { label: 'q2', query: 'query2', count: 1 }
]; ];
const data = { const data = {
list: { list: {
@@ -171,11 +174,11 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data); component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(2); 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', () => { it('should not fetch facet queries from response payload', () => {
component.responseFacetQueries = []; component.responseFacetQueries = new ResponseFacetQueryList();
const data = { const data = {
list: { list: {
@@ -192,11 +195,11 @@ describe('SearchSettingsComponent', () => {
it('should restore checked state for new response facet queries', () => { it('should restore checked state for new response facet queries', () => {
component.selectedFacetQueries = ['q3']; component.selectedFacetQueries = ['q3'];
component.responseFacetQueries = []; component.responseFacetQueries = new ResponseFacetQueryList();
const queries = [ const queries = [
{ label: 'q1', query: 'query1' }, { label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2' } { label: 'q2', query: 'query2', count: 1 }
]; ];
const data = { const data = {
list: { list: {
@@ -209,17 +212,17 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data); component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(2); expect(component.responseFacetQueries.length).toBe(2);
expect((<any> component.responseFacetQueries[0]).$checked).toBeFalsy(); expect((<any> component.responseFacetQueries.items[0]).$checked).toBeFalsy();
expect((<any> component.responseFacetQueries[1]).$checked).toBeFalsy(); expect((<any> component.responseFacetQueries.items[1]).$checked).toBeFalsy();
}); });
it('should not restore checked state for new response facet queries', () => { it('should not restore checked state for new response facet queries', () => {
component.selectedFacetQueries = ['q2']; component.selectedFacetQueries = ['q2'];
component.responseFacetQueries = []; component.responseFacetQueries = new ResponseFacetQueryList();
const queries = [ const queries = [
{ label: 'q1', query: 'query1' }, { label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2' } { label: 'q2', query: 'query2', count: 1 }
]; ];
const data = { const data = {
list: { list: {
@@ -232,8 +235,8 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data); component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(2); expect(component.responseFacetQueries.length).toBe(2);
expect((<any> component.responseFacetQueries[0]).$checked).toBeFalsy(); expect((<any> component.responseFacetQueries.items[0]).$checked).toBeFalsy();
expect((<any> component.responseFacetQueries[1]).$checked).toBeTruthy(); expect((<any> component.responseFacetQueries.items[1]).$checked).toBeTruthy();
}); });
it('should fetch facet fields from response payload', () => { it('should fetch facet fields from response payload', () => {
@@ -309,7 +312,7 @@ describe('SearchSettingsComponent', () => {
}); });
it('should reset queries and fields on empty response payload', () => { it('should reset queries and fields on empty response payload', () => {
component.responseFacetQueries = [<any> {}, <any> {}]; component.responseFacetQueries = new ResponseFacetQueryList([<any> {}, <any> {}]);
component.responseFacetFields = [<any> {}, <any> {}]; component.responseFacetFields = [<any> {}, <any> {}];
const data = { const data = {

View File

@@ -19,11 +19,11 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material'; import { MatCheckboxChange } from '@angular/material';
import { SearchService } from '@alfresco/adf-core'; import { SearchService } from '@alfresco/adf-core';
import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { FacetQuery } from '../../facet-query.interface';
import { ResponseFacetField } from '../../response-facet-field.interface'; import { ResponseFacetField } from '../../response-facet-field.interface';
import { FacetFieldBucket } from '../../facet-field-bucket.interface'; import { FacetFieldBucket } from '../../facet-field-bucket.interface';
import { SearchCategory } from '../../search-category.interface'; import { SearchCategory } from '../../search-category.interface';
import { ResponseFacetQuery } from '../../response-facet-query.interface'; import { ResponseFacetQuery } from '../../response-facet-query.interface';
import { ResponseFacetQueryList } from './models/response-facet-query-list.model';
@Component({ @Component({
selector: 'adf-search-filter', selector: 'adf-search-filter',
@@ -36,10 +36,20 @@ export class SearchFilterComponent implements OnInit {
selectedFacetQueries: string[] = []; selectedFacetQueries: string[] = [];
selectedBuckets: FacetFieldBucket[] = []; selectedBuckets: FacetFieldBucket[] = [];
responseFacetQueries: FacetQuery[] = []; responseFacetQueries: ResponseFacetQueryList;
responseFacetFields: ResponseFacetField[] = []; responseFacetFields: ResponseFacetField[] = [];
facetQueriesLabel: string = 'Facet Queries';
facetQueriesPageSize = 5;
facetQueriesExpanded = false;
constructor(public queryBuilder: SearchQueryBuilderService, private search: SearchService) { 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.updated.subscribe(query => {
this.queryBuilder.execute(); this.queryBuilder.execute();
}); });
@@ -138,11 +148,13 @@ export class SearchFilterComponent implements OnInit {
const context = data.list.context; const context = data.list.context;
if (context) { if (context) {
this.responseFacetQueries = (context.facetQueries || []).map(q => { const facetQueries = (context.facetQueries || []).map(q => {
q.$checked = this.selectedFacetQueries.includes(q.label); q.$checked = this.selectedFacetQueries.includes(q.label);
return q; return q;
}); });
this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize);
const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label); const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label);
this.responseFacetFields = (context.facetsFields || []).map( this.responseFacetFields = (context.facetsFields || []).map(
@@ -180,7 +192,7 @@ export class SearchFilterComponent implements OnInit {
} }
); );
} else { } else {
this.responseFacetQueries = []; this.responseFacetQueries = new ResponseFacetQueryList([], this.facetQueriesPageSize);
this.responseFacetFields = []; this.responseFacetFields = [];
} }
} }

View File

@@ -1,8 +1,15 @@
<mat-slider <mat-slider
[value]="value" [(value)]="value"
[disabled]="disabled"
[min]="min" [min]="min"
[max]="max" [max]="max"
[step]="step" [step]="step"
[thumbLabel]="thumbLabel" [thumbLabel]="thumbLabel"
(change)="onChangedHandler($event)"> (change)="onChangedHandler($event)">
</mat-slider> </mat-slider>
<div>
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>
</div>

View File

@@ -70,4 +70,62 @@ describe('SearchSliderComponent', () => {
expect(context.update).toHaveBeenCalled(); 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();
});
}); });

View File

@@ -15,7 +15,7 @@
* limitations under the License. * 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 { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface'; import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchQueryBuilderService } from '../../search-query-builder.service';
@@ -37,7 +37,9 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
min: number; min: number;
max: number; max: number;
thumbLabel = false; thumbLabel = false;
value: number;
@Input()
value: number | null;
ngOnInit() { ngOnInit() {
if (this.settings) { 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) { onChangedHandler(event: MatSliderChange) {
this.value = event.value; this.value = event.value;
this.updateQuery(this.value);
}
private updateQuery(value: number | null) {
if (this.id && this.context && this.settings && this.settings.field) { 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(); this.context.update();
} }
} }

View File

@@ -2,6 +2,9 @@
<input <input
matInput matInput
[placeholder]="settings?.placeholder" [placeholder]="settings?.placeholder"
[value]="value" [(ngModel)]="value"
(change)="onChangedHandler($event)"> (change)="onChangedHandler($event)">
<button mat-button *ngIf="value" matSuffix mat-icon-button (click)="reset()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field> </mat-form-field>

View File

@@ -0,0 +1,5 @@
.adf-search-text {
.mat-input-container {
width: 100%
}
}

View File

@@ -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 = <any> {
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('');
});
});

View File

@@ -23,6 +23,7 @@ import { SearchQueryBuilderService } from '../../search-query-builder.service';
@Component({ @Component({
selector: 'adf-search-text', selector: 'adf-search-text',
templateUrl: './search-text.component.html', templateUrl: './search-text.component.html',
styleUrls: ['./search-text.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-text' } host: { class: 'adf-search-text' }
}) })
@@ -36,7 +37,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
context: SearchQueryBuilderService; context: SearchQueryBuilderService;
ngOnInit() { ngOnInit() {
if (this.context && this.settings) { if (this.context && this.settings && this.settings.pattern) {
const pattern = new RegExp(this.settings.pattern, 'g'); const pattern = new RegExp(this.settings.pattern, 'g');
const match = pattern.exec(this.context.queryFragments[this.id] || ''); 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) { onChangedHandler(event) {
this.value = event.target.value; this.value = event.target.value;
if (this.value) { this.updateQuery(this.value);
this.context.queryFragments[this.id] = `${this.settings.field}:'${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(); this.context.update();
} }
} }

View File

@@ -19,3 +19,9 @@ export interface FacetQuery {
query: string; query: string;
label: string; label: string;
} }
export interface ResponseFacetQuery {
label?: string;
filterQuery?: string;
count?: number;
}

View File

@@ -25,6 +25,11 @@ export interface SearchConfiguration {
fields?: Array<string>; fields?: Array<string>;
categories: Array<SearchCategory>; categories: Array<SearchCategory>;
filterQueries?: Array<FilterQuery>; filterQueries?: Array<FilterQuery>;
facetQueries?: Array<FacetQuery>; facetQueries?: {
label?: string;
pageSize?: number;
expanded?: boolean;
queries: Array<FacetQuery>;
};
facetFields?: Array<FacetField>; facetFields?: Array<FacetField>;
} }

View File

@@ -120,10 +120,12 @@ describe('SearchQueryBuilder', () => {
it('should fetch facet query from config', () => { it('should fetch facet query from config', () => {
const config: SearchConfiguration = { const config: SearchConfiguration = {
categories: [], categories: [],
facetQueries: [ facetQueries: {
queries: [
{ query: 'q1', label: 'query1' }, { query: 'q1', label: 'query1' },
{ query: 'q2', label: 'query2' } { query: 'q2', label: 'query2' }
] ]
}
}; };
const builder = new SearchQueryBuilderService(buildConfig(config), null); const builder = new SearchQueryBuilderService(buildConfig(config), null);
const query = builder.getFacetQuery('query2'); const query = builder.getFacetQuery('query2');
@@ -135,9 +137,11 @@ describe('SearchQueryBuilder', () => {
it('should not fetch empty facet query from the config', () => { it('should not fetch empty facet query from the config', () => {
const config: SearchConfiguration = { const config: SearchConfiguration = {
categories: [], categories: [],
facetQueries: [ facetQueries: {
queries: [
{ query: 'q1', label: 'query1' } { query: 'q1', label: 'query1' }
] ]
}
}; };
const builder = new SearchQueryBuilderService(buildConfig(config), null); const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -273,15 +277,17 @@ describe('SearchQueryBuilder', () => {
categories: [ categories: [
<any> { id: 'cat1', enabled: true } <any> { id: 'cat1', enabled: true }
], ],
facetQueries: [ facetQueries: {
queries: [
{ query: 'q1', label: 'q2' } { query: 'q1', label: 'q2' }
] ]
}
}; };
const builder = new SearchQueryBuilderService(buildConfig(config), null); const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test'; builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery(); 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', () => { it('should build query with custom facet fields', () => {

View File

@@ -69,7 +69,7 @@ export class SearchQueryBuilderService {
getFacetQuery(label: string): FacetQuery { getFacetQuery(label: string): FacetQuery {
if (label) { if (label) {
const queries = this.config.facetQueries || []; const queries = this.config.facetQueries.queries || [];
return queries.find(q => q.label === label); return queries.find(q => q.label === label);
} }
return null; return null;
@@ -115,7 +115,7 @@ export class SearchQueryBuilderService {
paging: this.paging, paging: this.paging,
fields: this.config.fields, fields: this.config.fields,
filterQueries: this.filterQueries, filterQueries: this.filterQueries,
facetQueries: this.config.facetQueries, facetQueries: this.facetQueries,
facetFields: this.facetFields facetFields: this.facetFields
}; };
@@ -125,6 +125,18 @@ export class SearchQueryBuilderService {
return null; 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 <FacetQuery> { ...query };
});
}
return null;
}
private get facetFields(): RequestFacetFields { private get facetFields(): RequestFacetFields {
const facetFields = this.config.facetFields; const facetFields = this.config.facetFields;

View File

@@ -539,6 +539,23 @@
} }
}, },
"facetQueries": { "facetQueries": {
"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", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
@@ -548,6 +565,8 @@
"label": { "type": "string" } "label": { "type": "string" }
} }
} }
}
}
}, },
"categories": { "categories": {
"type": "array", "type": "array",