[ADF-2846] new facets (#3251)

* schema and configuration improvements

* check list search widget

* "Clear all" button to reset the list

* page size and "show more" for response facet fields

* test fixes

* fix tests

* fix pagination bug (skipCount reseting)

* integrate date range picker from #3248

* i18n support for date and number range

* some docs for search filter

* docs update

* docs update

* cleanup code as per review
This commit is contained in:
Denys Vuika
2018-05-01 14:49:03 +01:00
committed by Eugenio Romano
parent bb664a283b
commit dbe88a5efc
34 changed files with 1161 additions and 244 deletions

View File

@@ -392,9 +392,6 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
this.loadFolder();
} else if (this.data) {
if (changes.node && changes.node.currentValue) {
if (changes.node.currentValue.list.pagination) {
changes.node.currentValue.list.pagination.skipCount = 0;
}
this.data.loadPage(changes.node.currentValue);
this.onDataReady(changes.node.currentValue);
} else if (changes.rowFilter) {

View File

@@ -173,11 +173,19 @@
"FILTER": {
"ACTIONS": {
"CLEAR": "Clear",
"APPLY": "Apply"
"APPLY": "Apply",
"CLEAR-ALL": "Clear all"
},
"RANGE": {
"FROM": "From",
"TO": "To"
"TO": "To",
"FROM-DATE": "Choose a date",
"TO-DATE": "Choose a date"
},
"VALIDATION": {
"REQUIRED-VALUE": "Required value",
"NO-DAYS": "No days selected.",
"INVALID-FORMAT": "Invalid Format"
}
},
"ICONS": {

View File

@@ -0,0 +1,12 @@
<mat-checkbox
*ngFor="let option of options"
[checked]="option.checked"
(change)="changeHandler($event, option)">
{{ option.name | translate }}
</mat-checkbox>
<div>
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
</button>
</div>

View File

@@ -0,0 +1,8 @@
.adf-search-check-list {
display: flex;
flex-direction: column;
.mat-checkbox {
margin: 5px;
}
}

View File

@@ -0,0 +1,118 @@
/*!
* @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 { SearchCheckListComponent } from './search-check-list.component';
describe('SearchCheckListComponent', () => {
let component: SearchCheckListComponent;
beforeEach(() => {
component = new SearchCheckListComponent();
});
it('should setup options from settings', () => {
const options: any = [
{ 'name': 'Folder', 'value': "TYPE:'cm:folder'" },
{ 'name': 'Document', 'value': "TYPE:'cm:content'" }
];
component.settings = <any> { options: options };
component.ngOnInit();
expect(component.options).toEqual(options);
});
it('should setup operator from the settings', () => {
component.settings = <any> { operator: 'AND' };
component.ngOnInit();
expect(component.operator).toBe('AND');
});
it('should use OR operator by default', () => {
component.settings = <any> { operator: null };
component.ngOnInit();
expect(component.operator).toBe('OR');
});
it('should update query builder on checkbox change', () => {
component.options = [
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: false },
{ name: 'Document', value: "TYPE:'cm:content'", checked: false }
];
component.id = 'checklist';
component.context = <any> {
queryFragments: {},
update() {}
};
component.ngOnInit();
spyOn(component.context, 'update').and.stub();
component.changeHandler(
<any> { checked: true },
component.options[0]
);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`);
component.changeHandler(
<any> { checked: true },
component.options[1]
);
expect(component.context.queryFragments[component.id]).toEqual(
`TYPE:'cm:folder' OR TYPE:'cm:content'`
);
});
it('should reset selected boxes', () => {
component.options = [
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: true },
{ name: 'Document', value: "TYPE:'cm:content'", checked: true }
];
component.reset();
expect(component.options[0].checked).toBeFalsy();
expect(component.options[1].checked).toBeFalsy();
});
it('should update query builder on reset', () => {
component.id = 'checklist';
component.context = <any> {
queryFragments: {
'checklist': 'query'
},
update() {}
};
spyOn(component.context, 'update').and.stub();
component.ngOnInit();
component.options = [
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: true },
{ name: 'Document', value: "TYPE:'cm:content'", checked: true }
];
component.reset();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe('');
});
});

View File

@@ -0,0 +1,77 @@
/*!
* @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 { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
@Component({
selector: 'adf-search-check-list',
templateUrl: './search-check-list.component.html',
styleUrls: ['./search-check-list.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-check-list' }
})
export class SearchCheckListComponent implements SearchWidget, OnInit {
id: string;
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
options: { name: string, value: string, checked: boolean }[] = [];
operator: string = 'OR';
ngOnInit(): void {
if (this.settings) {
this.operator = this.settings.operator || 'OR';
if (this.settings.options && this.settings.options.length > 0) {
this.options = [...this.settings.options];
}
}
}
reset() {
this.options.forEach(opt => {
opt.checked = false;
});
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.update();
}
}
changeHandler(event: MatCheckboxChange, option: any) {
option.checked = event.checked;
this.flush();
}
flush() {
const checkedValues = this.options
.filter(option => option.checked)
.map(option => option.value);
const query = checkedValues.join(` ${this.operator} `);
if (this.id && this.context) {
this.context.queryFragments[this.id] = query;
this.context.update();
}
}
}

View File

@@ -0,0 +1,36 @@
<form [formGroup]="form" novalidate (ngSubmit)="apply(form.value, form.valid)">
<mat-form-field>
<input matInput [formControl]="from" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM-DATE' | translate }}"
[matDatepicker]="fromDatepicker"
[max]="maxFrom">
<mat-datepicker-toggle matSuffix [for]="fromDatepicker"></mat-datepicker-toggle>
<mat-datepicker #fromDatepicker></mat-datepicker>
<mat-error *ngIf="from.hasError('required')">
{{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput [formControl]="to" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.TO-DATE' | translate }}"
[matDatepicker]="toDatepicker"
[min]="from.value">
<mat-datepicker-toggle matSuffix [for]="toDatepicker"></mat-datepicker-toggle>
<mat-datepicker #toDatepicker></mat-datepicker>
<mat-error *ngIf="!hasSelectedDays(from.value, to.value)">
{{ 'SEARCH.FILTER.VALIDATION.NO-DAYS' | translate }}
</mat-error>
</mat-form-field>
<div>
<button mat-button color="primary" type="button" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>
<button mat-button color="primary" type="submit" [disabled]="!form.valid">
{{ 'SEARCH.FILTER.ACTIONS.APPLY' | translate }}
</button>
</div>
</form>

View File

@@ -0,0 +1,8 @@
.adf-search-date-range > form {
display: inline-flex;
flex-direction: column;
.mat-button {
text-transform: uppercase;
}
}

View File

@@ -0,0 +1,98 @@
/*!
* @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 { SearchDateRangeComponent } from './search-date-range.component';
import moment from 'moment-es6';
describe('SearchDateRangeComponent', () => {
let component: SearchDateRangeComponent;
let fromDate = '2016-10-16';
let toDate = '2017-10-16';
beforeEach(() => {
component = new SearchDateRangeComponent();
});
it('should setup form elements on init', () => {
component.ngOnInit();
expect(component.form).toBeDefined();
expect(component.to).toBeDefined();
expect(component.form).toBeDefined();
});
it('should reset form', () => {
component.ngOnInit();
component.form.setValue({ from: fromDate, to: toDate });
expect(component.from.value).toEqual(fromDate);
expect(component.to.value).toEqual(toDate);
component.reset();
expect(component.from.value).toEqual('');
expect(component.to.value).toEqual('');
expect(component.form.value).toEqual({ from: '', to: '' });
});
it('should update query builder on reset', () => {
const context: any = {
queryFragments: {
createdDateRange: 'query'
},
update() {}
};
component.id = 'createdDateRange';
component.context = context;
spyOn(context, 'update').and.stub();
component.ngOnInit();
component.reset();
expect(context.queryFragments.createdDateRange).toEqual('');
expect(context.update).toHaveBeenCalled();
});
it('should update query builder on value changes', () => {
const context: any = {
queryFragments: {},
update() {}
};
component.id = 'createdDateRange';
component.context = context;
component.settings = { field: 'cm:created' };
spyOn(context, 'update').and.stub();
component.ngOnInit();
component.apply({
from: fromDate,
to: toDate
}, true);
const startDate = moment(fromDate).startOf('day').format();
const endDate = moment(toDate).endOf('day').format();
const expectedQuery = `cm:created:['${startDate}' TO '${endDate}']`;
expect(context.queryFragments[component.id]).toEqual(expectedQuery);
expect(context.update).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,92 @@
/*!
* @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 { OnInit, Component, ViewEncapsulation } from '@angular/core';
import { FormControl, Validators, FormGroup } from '@angular/forms';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import moment from 'moment-es6';
@Component({
selector: 'adf-search-date-range',
templateUrl: './search-date-range.component.html',
styleUrls: ['./search-date-range.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-date-range' }
})
export class SearchDateRangeComponent implements SearchWidget, OnInit {
from: FormControl;
to: FormControl;
form: FormGroup;
matcher = new LiveErrorStateMatcher();
id: string;
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
maxFrom: any;
ngOnInit() {
const validators = Validators.compose([
Validators.required
]);
this.from = new FormControl('', validators);
this.to = new FormControl('', validators);
this.form = new FormGroup({
from: this.from,
to: this.to
});
this.maxFrom = moment().startOf('day');
}
apply(model: { from: string, to: string }, isValid: boolean) {
if (isValid && this.id && this.context && this.settings && this.settings.field) {
const start = moment(model.from).startOf('day').format();
const end = moment(model.to).endOf('day').format();
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
this.context.update();
}
}
reset() {
this.form.reset({
from: '',
to: ''
});
if (this.id && this.context) {
this.context.queryFragments[this.id] = '';
this.context.update();
}
}
hasSelectedDays(from: string, to: string): boolean {
if (from && to) {
const start = moment(from).startOf('day');
const end = moment(to).endOf('day');
return start.isBefore(end);
}
return true;
}
}

View File

@@ -35,7 +35,7 @@
<mat-expansion-panel
*ngFor="let field of responseFacetFields"
[expanded]="field.$expanded"
[expanded]="field.expanded"
(opened)="onFacetFieldExpanded(field)"
(closed)="onFacetFieldCollapsed(field)">
<mat-expansion-panel-header>
@@ -43,12 +43,19 @@
</mat-expansion-panel-header>
<div class="checklist">
<mat-checkbox
*ngFor="let bucket of field.buckets"
*ngFor="let bucket of field.getVisibleBuckets()"
[checked]="bucket.$checked"
(change)="onFacetToggle($event, field, bucket)">
{{ bucket.display || bucket.label }} ({{ bucket.count }})
</mat-checkbox>
</div>
<button mat-button
*ngIf="field.hasMoreItems()"
(click)="field.showMoreItems()">
Show more
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</mat-expansion-panel>
</mat-accordion>

View File

@@ -64,19 +64,19 @@ describe('SearchSettingsComponent', () => {
});
it('should update facet field model on expand', () => {
const field: any = { $expanded: false };
const field: any = { expanded: false };
component.onFacetFieldExpanded(field);
expect(field.$expanded).toBeTruthy();
expect(field.expanded).toBeTruthy();
});
it('should update facet field model on collapse', () => {
const field: any = { $expanded: true };
const field: any = { expanded: true };
component.onFacetFieldCollapsed(field);
expect(field.$expanded).toBeFalsy();
expect(field.expanded).toBeFalsy();
});
it('should update bucket model and query builder on facet toggle', () => {
@@ -118,6 +118,7 @@ describe('SearchSettingsComponent', () => {
it('should unselect facet query and update builder', () => {
const config: SearchConfiguration = {
categories: [],
facetQueries: [
{ label: 'q1', query: 'query1' }
]
@@ -238,7 +239,7 @@ describe('SearchSettingsComponent', () => {
it('should fetch facet fields from response payload', () => {
component.responseFacetFields = [];
const fields = [
const fields: any = [
{ label: 'f1', buckets: [] },
{ label: 'f2', buckets: [] }
];
@@ -256,9 +257,9 @@ describe('SearchSettingsComponent', () => {
});
it('should restore expanded state for new response facet fields', () => {
component.responseFacetFields = [
component.responseFacetFields = <any> [
{ label: 'f1', buckets: [] },
{ label: 'f2', buckets: [], $expanded: true }
{ label: 'f2', buckets: [], expanded: true }
];
const fields = [
@@ -276,8 +277,8 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data);
expect(component.responseFacetFields.length).toBe(2);
expect(component.responseFacetFields[0].$expanded).toBeFalsy();
expect(component.responseFacetFields[1].$expanded).toBeTruthy();
expect(component.responseFacetFields[0].expanded).toBeFalsy();
expect(component.responseFacetFields[1].expanded).toBeTruthy();
});
it('should restore checked buckets for new response facet fields', () => {
@@ -285,7 +286,7 @@ describe('SearchSettingsComponent', () => {
const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' };
component.selectedBuckets = [ bucket2 ];
component.responseFacetFields = [
component.responseFacetFields = <any> [
{ label: 'f2', buckets: [] }
];

View File

@@ -63,11 +63,11 @@ export class SearchFilterComponent implements OnInit {
}
onFacetFieldExpanded(field: ResponseFacetField) {
field.$expanded = true;
field.expanded = true;
}
onFacetFieldCollapsed(field: ResponseFacetField) {
field.$expanded = false;
field.expanded = false;
}
onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) {
@@ -143,11 +143,13 @@ export class SearchFilterComponent implements OnInit {
return q;
});
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(
(field: ResponseFacetField) => {
field.$expanded = expandedFields.includes(field.label);
field.pageSize = field.pageSize || 5;
field.currentPageSize = field.pageSize;
field.expanded = expandedFields.includes(field.label);
(field.buckets || []).forEach(bucket => {
bucket.$field = field.label;
@@ -160,6 +162,20 @@ export class SearchFilterComponent implements OnInit {
bucket.$checked = true;
}
});
field.hasMoreItems = (): boolean => {
return field.buckets && field.buckets.length > 0 && field.buckets.length > field.currentPageSize;
};
field.showMoreItems = () => {
field.currentPageSize += field.pageSize;
};
field.getVisibleBuckets = (): FacetFieldBucket[] => {
const buckets = field.buckets || [];
return buckets.slice(0, field.currentPageSize);
};
return field;
}
);
@@ -168,5 +184,4 @@ export class SearchFilterComponent implements OnInit {
this.responseFacetFields = [];
}
}
}

View File

@@ -20,6 +20,8 @@ import { SearchTextComponent } from '../search-text/search-text.component';
import { SearchRadioComponent } from '../search-radio/search-radio.component';
import { SearchSliderComponent } from '../search-slider/search-slider.component';
import { SearchNumberRangeComponent } from '../search-number-range/search-number-range.component';
import { SearchCheckListComponent } from '../search-check-list/search-check-list.component';
import { SearchDateRangeComponent } from '../search-date-range/search-date-range.component';
@Injectable()
export class SearchFilterService {
@@ -31,7 +33,9 @@ export class SearchFilterService {
'text': SearchTextComponent,
'radio': SearchRadioComponent,
'slider': SearchSliderComponent,
'number-range': SearchNumberRangeComponent
'number-range': SearchNumberRangeComponent,
'check-list': SearchCheckListComponent,
'date-range': SearchDateRangeComponent
};
}

View File

@@ -4,15 +4,21 @@
<input
matInput [formControl]="from" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM' | translate }}">
<mat-error *ngIf="from.hasError('pattern')">Invalid format</mat-error>
<mat-error *ngIf="from.hasError('required')">Required value</mat-error>
<mat-error *ngIf="from.hasError('pattern')">
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
</mat-error>
<mat-error *ngIf="from.hasError('required')">
{{ 'SEARCH.FILTER.VALIDATION.REQUIRED-VALUE' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field >
<input
matInput [formControl]="to" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.TO' | translate }}">
<mat-error *ngIf="to.invalid">Invalid format</mat-error>
<mat-error *ngIf="to.invalid">
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
</mat-error>
</mat-form-field>

View File

@@ -19,6 +19,9 @@ export interface FacetField {
field: string;
label: string;
mincount?: number;
limit?: number;
offset?: number;
prefix?: string;
$checked?: boolean;
}

View File

@@ -20,6 +20,11 @@ import { FacetFieldBucket } from './facet-field-bucket.interface';
export interface ResponseFacetField {
label: string;
buckets: Array<FacetFieldBucket>;
pageSize?: number;
currentPageSize?: number;
expanded?: boolean;
$expanded?: boolean;
hasMoreItems(): boolean;
showMoreItems(): void;
getVisibleBuckets(): Array<FacetFieldBucket>;
}

View File

@@ -23,12 +23,8 @@ import { SearchCategory } from './search-category.interface';
export interface SearchConfiguration {
include?: Array<string>;
fields?: Array<string>;
query?: {
categories: Array<SearchCategory>
};
categories: Array<SearchCategory>;
filterQueries?: Array<FilterQuery>;
facetQueries?: Array<FacetQuery>;
facetFields?: {
facets: Array<FacetField>
};
facetFields?: Array<FacetField>;
}

View File

@@ -37,13 +37,11 @@ describe('SearchQueryBuilder', () => {
it('should use only enabled categories', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: false },
<any> { id: 'cat3', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: false },
<any> { id: 'cat3', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -54,9 +52,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch filter queries from config', () => {
const config: SearchConfiguration = {
query: {
categories: []
},
categories: [],
filterQueries: [
{ query: 'query1' },
{ query: 'query2' }
@@ -123,6 +119,7 @@ describe('SearchQueryBuilder', () => {
it('should fetch facet query from config', () => {
const config: SearchConfiguration = {
categories: [],
facetQueries: [
{ query: 'q1', label: 'query1' },
{ query: 'q2', label: 'query2' }
@@ -137,6 +134,7 @@ describe('SearchQueryBuilder', () => {
it('should not fetch empty facet query from the config', () => {
const config: SearchConfiguration = {
categories: [],
facetQueries: [
{ query: 'q1', label: 'query1' }
]
@@ -179,11 +177,9 @@ describe('SearchQueryBuilder', () => {
it('should require a query fragment to build query', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = null;
@@ -194,11 +190,9 @@ describe('SearchQueryBuilder', () => {
it('should build query with single fragment', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -210,12 +204,10 @@ describe('SearchQueryBuilder', () => {
it('should build query with multiple fragments', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -231,12 +223,10 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom fields', () => {
const config: SearchConfiguration = {
fields: ['field1', 'field2'],
query: {
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -249,12 +239,10 @@ describe('SearchQueryBuilder', () => {
it('should build query with empty custom fields', () => {
const config: SearchConfiguration = {
fields: [],
query: {
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
@@ -266,11 +254,9 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom filter queries', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test';
@@ -284,11 +270,9 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom facet queries', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
},
categories: [
<any> { id: 'cat1', enabled: true }
],
facetQueries: [
{ query: 'q1', label: 'q2' }
]
@@ -302,32 +286,26 @@ describe('SearchQueryBuilder', () => {
it('should build query with custom facet fields', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
},
facetFields: {
facets: [
{ field: 'field1', label: 'field1' },
{ field: 'field2', label: 'field2' }
]
}
categories: [
<any> { id: 'cat1', enabled: true }
],
facetFields: [
{ field: 'field1', label: 'field1', mincount: 1, limit: null, offset: 0, prefix: null },
{ field: 'field2', label: 'field2', mincount: 1, limit: null, offset: 0, prefix: null }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
expect(compiled.facetFields).toEqual(config.facetFields);
expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields));
});
it('should use pagination settings', () => {
const config: SearchConfiguration = {
query: {
categories: [
<any> { id: 'cat1', enabled: true }
]
}
categories: [
<any> { id: 'cat1', enabled: true }
]
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test';

View File

@@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { QueryBody } from 'alfresco-js-api';
import { QueryBody, RequestFacetFields, RequestFacetField } from 'alfresco-js-api';
import { SearchCategory } from './search-category.interface';
import { FilterQuery } from './filter-query.interface';
import { SearchRange } from './search-range.interface';
@@ -45,8 +45,8 @@ export class SearchQueryBuilderService {
throw new Error('Search configuration not found.');
}
if (this.config.query && this.config.query.categories) {
this.categories = this.config.query.categories.filter(f => f.enabled);
if (this.config.categories) {
this.categories = this.config.categories.filter(f => f.enabled);
}
this.filterQueries = this.config.filterQueries || [];
@@ -116,7 +116,7 @@ export class SearchQueryBuilderService {
fields: this.config.fields,
filterQueries: this.filterQueries,
facetQueries: this.config.facetQueries,
facetFields: this.config.facetFields
facetFields: this.facetFields
};
return result;
@@ -124,4 +124,23 @@ export class SearchQueryBuilderService {
return null;
}
private get facetFields(): RequestFacetFields {
const facetFields = this.config.facetFields;
if (facetFields && facetFields.length > 0) {
return {
facets: facetFields.map(facet => <RequestFacetField> {
field: facet.field,
mincount: facet.mincount,
label: facet.label,
limit: facet.limit,
offset: facet.offset,
prefix: facet.prefix
})
};
}
return null;
}
}

View File

@@ -35,6 +35,8 @@ import { SearchTextComponent } from './components/search-text/search-text.compon
import { SearchRadioComponent } from './components/search-radio/search-radio.component';
import { SearchSliderComponent } from './components/search-slider/search-slider.component';
import { SearchNumberRangeComponent } from './components/search-number-range/search-number-range.component';
import { SearchCheckListComponent } from './components/search-check-list/search-check-list.component';
import { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component';
export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
SearchComponent,
@@ -60,7 +62,9 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
SearchTextComponent,
SearchRadioComponent,
SearchSliderComponent,
SearchNumberRangeComponent
SearchNumberRangeComponent,
SearchCheckListComponent,
SearchDateRangeComponent
],
exports: [
...ALFRESCO_SEARCH_DIRECTIVES,
@@ -68,14 +72,18 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
SearchTextComponent,
SearchRadioComponent,
SearchSliderComponent,
SearchNumberRangeComponent
SearchNumberRangeComponent,
SearchCheckListComponent,
SearchDateRangeComponent
],
entryComponents: [
SearchWidgetContainerComponent,
SearchTextComponent,
SearchRadioComponent,
SearchSliderComponent,
SearchNumberRangeComponent
SearchNumberRangeComponent,
SearchCheckListComponent,
SearchDateRangeComponent
]
})
export class SearchModule {}