[ADF-3497] Facet intervals on search filter (#4255)

* [ADF-3497] allow facetIntervals on search

* [ADF-3497] update schema json

* [ADF-3497] update json

* [ADF-3497] documentation update

* [ADF-3497] specify that sets are not supported

* [ADF-3497] no spaces on labels - mention

* [ADF-3497] documentation update

* [ADF-3497] update examples & document label key

* [ADF-3497] tests added

* [ADF-3497] testRail id

* [ADF-3497] allow config of custom pageSize values

* [ADF-3497] support mincount filtering also for intervals

* [ADF-3497] support expanded property also for facetIntervals

* remove no longer needed info

- bcs. of PR #4322 fix
This commit is contained in:
Suzana Dirla
2019-02-18 20:51:15 +02:00
committed by Eugenio Romano
parent f20a71438c
commit e34f80aff7
11 changed files with 368 additions and 21 deletions

View File

@@ -202,8 +202,8 @@ describe('SearchFilterComponent', () => {
queryBuilder.config = {
categories: [],
facetFields: { fields: [
{ label: 'f1', field: 'f1' },
{ label: 'f2', field: 'f2' }
{ label: 'f1', field: 'f1', mincount: 0 },
{ label: 'f2', field: 'f2', mincount: 0 }
]},
facetQueries: {
queries: []
@@ -593,4 +593,92 @@ describe('SearchFilterComponent', () => {
expect(entry.checked).toBeFalsy();
}
});
it('should fetch facet intervals from response payload', () => {
component.responseFacets = null;
queryBuilder.config = {
categories: [],
facetIntervals: {
intervals: [
{ label: 'test_intervals1', field: 'f1', sets: [
{ label: 'interval1', start: 's1', end: 'e1'},
{ label: 'interval2', start: 's2', end: 'e2'}
]},
{ label: 'test_intervals2', field: 'f2', sets: [
{ label: 'interval3', start: 's3', end: 'e3'},
{ label: 'interval4', start: 's4', end: 'e4'}
]}
]
}
};
const response1 = [
{ label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]},
{ label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]}
];
const response2 = [
{ label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]},
{ label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]}
];
const data = {
list: {
context: {
facets: [
{ type: 'interval', label: 'test_intervals1', buckets: response1 },
{ type: 'interval', label: 'test_intervals2', buckets: response2 }
]
}
}
};
component.onDataLoaded(data);
expect(component.responseFacets.length).toBe(2);
expect(component.responseFacets[0].buckets.length).toEqual(2);
expect(component.responseFacets[1].buckets.length).toEqual(2);
});
it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => {
component.responseFacets = null;
queryBuilder.config = {
categories: [],
facetIntervals: {
intervals: [
{ label: 'test_intervals1', field: 'f1', mincount: 2, sets: [
{ label: 'interval1', start: 's1', end: 'e1'},
{ label: 'interval2', start: 's2', end: 'e2'}
]},
{ label: 'test_intervals2', field: 'f2', mincount: 5, sets: [
{ label: 'interval3', start: 's3', end: 'e3'},
{ label: 'interval4', start: 's4', end: 'e4'}
]}
]
}
};
const response1 = [
{ label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]},
{ label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]}
];
const response2 = [
{ label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]},
{ label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]}
];
const data = {
list: {
context: {
facets: [
{ type: 'interval', label: 'test_intervals1', buckets: response1 },
{ type: 'interval', label: 'test_intervals2', buckets: response2 }
]
}
}
};
component.onDataLoaded(data);
expect(component.responseFacets.length).toBe(2);
expect(component.responseFacets[0].buckets.length).toEqual(1);
expect(component.responseFacets[1].buckets.length).toEqual(0);
});
});

View File

@@ -41,8 +41,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
facetQueriesLabel: string = 'Facet Queries';
facetQueriesExpanded = false;
facetFieldsExpanded = false;
facetExpanded = {
'default': false
};
selectedBuckets: Array<{ field: FacetField, bucket: FacetFieldBucket }> = [];
@@ -52,10 +53,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
if (queryBuilder.config && queryBuilder.config.facetQueries) {
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE;
this.facetQueriesExpanded = queryBuilder.config.facetQueries.expanded;
this.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded;
}
if (queryBuilder.config && queryBuilder.config.facetFields) {
this.facetFieldsExpanded = queryBuilder.config.facetFields.expanded;
this.facetExpanded['field'] = queryBuilder.config.facetFields.expanded;
}
if (queryBuilder.config && queryBuilder.config.facetIntervals) {
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
}
this.queryBuilder.updated.pipe(
@@ -146,7 +150,7 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}
shouldExpand(field: FacetField): boolean {
return field.type === 'query' ? this.facetQueriesExpanded : this.facetFieldsExpanded;
return this.facetExpanded[field.type] || this.facetExpanded['default'];
}
onDataLoaded(data: any) {
@@ -162,8 +166,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private parseFacets(context: ResultSetContext) {
if (!this.responseFacets) {
const responseFacetFields = this.parseFacetFields(context);
const responseFacetIntervals = this.parseFacetIntervals(context);
const responseGroupedFacetQueries = this.parseFacetQueries(context);
this.responseFacets = responseFacetFields.concat(...responseGroupedFacetQueries);
this.responseFacets = responseFacetFields.concat(...responseGroupedFacetQueries, ...responseFacetIntervals);
} else {
this.responseFacets = this.responseFacets
@@ -184,12 +189,11 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}
}
private parseFacetFields(context: ResultSetContext): FacetField[] {
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
private parseFacetItems(context: ResultSetContext, configFacetFields, itemType): FacetField[] {
return configFacetFields.map((field) => {
const responseField = (context.facets || []).find((response) => response.type === 'field' && response.label === field.label) || {};
const responseBuckets = this.getResponseBuckets(responseField);
const responseField = (context.facets || []).find((response) => response.type === itemType && response.label === field.label) || {};
const responseBuckets = this.getResponseBuckets(responseField)
.filter(this.getFilterByMinCount(field.mincount));
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, field.pageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => {
@@ -212,6 +216,16 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
});
}
private parseFacetFields(context: ResultSetContext): FacetField[] {
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
return this.parseFacetItems(context, configFacetFields, 'field');
}
private parseFacetIntervals(context: ResultSetContext): FacetField[] {
const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || [];
return this.parseFacetItems(context, configFacetIntervals, 'interval');
}
private parseFacetQueries(context: ResultSetContext): FacetField[] {
const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || [];
const configGroups = configFacetQueries.reduce((acc, query) => {
@@ -225,10 +239,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}, []);
const result = [];
const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount;
const mincountFilter = this.getFilterByMinCount(mincount);
Object.keys(configGroups).forEach((group) => {
const responseField = (context.facets || []).find((response) => response.type === 'query' && response.label === group) || {};
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group]);
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group])
.filter(mincountFilter);
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, this.facetQueriesPageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => {
@@ -278,12 +295,6 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
display: respBucket.display,
label: respBucket.label
};
}).filter((bucket) => {
let mincount = this.queryBuilder.config.facetQueries.mincount;
if (mincount === undefined) {
mincount = 1;
}
return bucket.count >= mincount;
});
}
@@ -291,4 +302,14 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
return (!!bucket && !!bucket.metrics && bucket.metrics[0] && bucket.metrics[0].value && bucket.metrics[0].value.count)
|| 0;
}
private getFilterByMinCount(mincountInput: number) {
return (bucket) => {
let mincount = mincountInput;
if (mincount === undefined) {
mincount = 1;
}
return bucket.count >= mincount;
};
}
}

View File

@@ -38,6 +38,10 @@ export interface SearchConfiguration {
expanded?: boolean;
fields: Array<FacetField>;
};
facetIntervals?: {
expanded?: boolean;
intervals: Array<any>;
};
sorting?: {
options: Array<SearchSortingDefinition>;
defaults: Array<SearchSortingDefinition>;

View File

@@ -371,6 +371,39 @@ describe('SearchQueryBuilder', () => {
expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields.fields));
});
it('should build query with custom facet intervals', () => {
const config: SearchConfiguration = {
categories: [
<any> { id: 'cat1', enabled: true }
],
facetIntervals: {
intervals: [
{
label: 'test_intervals1',
field: 'f1',
sets: [
{ label: 'interval1', start: 's1', end: 'e1' },
{ label: 'interval2', start: 's2', end: 'e2' }
]
},
{
label: 'test_intervals2',
field: 'f2',
sets: [
{ label: 'interval3', start: 's3', end: 'e3' },
{ label: 'interval4', start: 's4', end: 'e4' }
]
}
]
}
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
expect(compiled.facetIntervals).toEqual(jasmine.objectContaining(config.facetIntervals));
});
it('should build query with sorting', () => {
const config: SearchConfiguration = {
fields: [],

View File

@@ -224,6 +224,7 @@ export class SearchQueryBuilderService {
fields: this.config.fields,
filterQueries: this.filterQueries,
facetQueries: this.facetQueries,
facetIntervals: this.facetIntervals,
facetFields: this.facetFields,
sort: this.sort
};
@@ -280,6 +281,20 @@ export class SearchQueryBuilderService {
return false;
}
/**
* Checks if FacetIntervals has been defined
* @returns True if defined, false otherwise
*/
get hasFacetIntervals(): boolean {
if (this.config
&& this.config.facetIntervals
&& this.config.facetIntervals.intervals
&& this.config.facetIntervals.intervals.length > 0) {
return true;
}
return false;
}
protected get sort(): RequestSortDefinitionInner[] {
return this.sorting.map((def) => {
return new RequestSortDefinitionInner({
@@ -301,6 +316,22 @@ export class SearchQueryBuilderService {
return null;
}
protected get facetIntervals(): any {
if (this.hasFacetIntervals) {
const configIntervals = this.config.facetIntervals;
return {
intervals: configIntervals.intervals.map((interval) => <any> {
label: interval.label,
field: interval.field,
sets: interval.sets
})
};
}
return null;
}
protected getFinalQuery(): string {
let query = '';