[ADF-3365] search filter fixes (#3594)

* preserve ordering of facet fields and queries

* update tests

* rework facet queries

* rework facet management, update unit tests

* remove unused interfaces

* fix test

* remove deprecated interfaces

* expose selection for the chip list
This commit is contained in:
Denys Vuika 2018-07-17 20:18:52 +01:00 committed by Eugenio Romano
parent 98243f0450
commit c63184334f
15 changed files with 484 additions and 665 deletions

View File

@ -83,24 +83,24 @@
{ "query": "NOT cm:creator:System" } { "query": "NOT cm:creator:System" }
], ],
"facetFields": [ "facetFields": [
{ "field": "content.mimetype", "mincount": 1, "label": "Type" }, { "field": "content.mimetype", "mincount": 1, "label": "1:Type" },
{ "field": "content.size", "mincount": 1, "label": "Size" }, { "field": "content.size", "mincount": 1, "label": "2:Size" },
{ "field": "creator", "mincount": 1, "label": "Creator" }, { "field": "creator", "mincount": 1, "label": "3:Creator" },
{ "field": "modifier", "mincount": 1, "label": "Modifier" }, { "field": "modifier", "mincount": 1, "label": "4:Modifier" },
{ "field": "created", "mincount": 1, "label": "Created" } { "field": "created", "mincount": 1, "label": "5:Created" }
], ],
"facetQueries": { "facetQueries": {
"label": "My facet queries", "label": "My facet queries",
"pageSize": 5, "pageSize": 5,
"queries": [ "queries": [
{ "query": "created:2018", "label": "Created This Year" }, { "query": "created:2018", "label": "1.Created This Year" },
{ "query": "content.mimetype", "label": "Type" }, { "query": "content.mimetype", "label": "2.Type" },
{ "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"}, { "query": "content.size:[0 TO 10240]", "label": "3.Size: xtra small"},
{ "query": "content.size:[10240 TO 102400]", "label": "Size: small"}, { "query": "content.size:[10240 TO 102400]", "label": "4.Size: small"},
{ "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" }, { "query": "content.size:[102400 TO 1048576]", "label": "5.Size: medium" },
{ "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" }, { "query": "content.size:[1048576 TO 16777216]", "label": "6.Size: large" },
{ "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" }, { "query": "content.size:[16777216 TO 134217728]", "label": "7.Size: xtra large" },
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" } { "query": "content.size:[134217728 TO MAX]", "label": "8.Size: XX large" }
] ]
}, },
"categories": [ "categories": [

View File

@ -1,14 +1,14 @@
<mat-chip-list> <mat-chip-list>
<ng-container *ngIf="searchFilter && searchFilter.selectedFacetQueries"> <ng-container *ngIf="searchFilter && searchFilter.selectedFacetQueries.length">
<mat-chip <mat-chip
*ngFor="let label of searchFilter.selectedFacetQueries" *ngFor="let query of searchFilter.selectedFacetQueries"
[removable]="true" [removable]="true"
(remove)="searchFilter.unselectFacetQuery(label)"> (remove)="searchFilter.unselectFacetQuery(query)">
{{ label | translate }} {{ query.label | translate }}
<mat-icon matChipRemove>cancel</mat-icon> <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip> </mat-chip>
</ng-container> </ng-container>
<ng-container *ngIf="searchFilter && searchFilter.selectedBuckets"> <ng-container *ngIf="searchFilter && searchFilter.selectedBuckets.length">
<mat-chip <mat-chip
*ngFor="let bucket of searchFilter.selectedBuckets" *ngFor="let bucket of searchFilter.selectedBuckets"
[removable]="true" [removable]="true"

View File

@ -15,23 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { ResponseFacetQuery } from '../../../facet-query.interface'; import { FacetQuery } from '../../../facet-query.interface';
import { SearchFilterList } from './search-filter-list.model'; import { SearchFilterList } from './search-filter-list.model';
export class ResponseFacetQueryList extends SearchFilterList<ResponseFacetQuery> { export class ResponseFacetQueryList extends SearchFilterList<FacetQuery> {
constructor(items: ResponseFacetQuery[] = [], pageSize: number = 5) { constructor(items: FacetQuery[] = [], pageSize: number = 5) {
super( super(
items items
.filter(item => { .filter(item => {
return item.count > 0; return item.count > 0;
})
.map(item => {
return <ResponseFacetQuery> { ...item };
}), }),
pageSize pageSize
); );
this.filter = (query: ResponseFacetQuery) => { this.filter = (query: FacetQuery) => {
if (this.filterText && query.label) { if (this.filterText && query.label) {
const pattern = (this.filterText || '').toLowerCase(); const pattern = (this.filterText || '').toLowerCase();
const label = query.label.toLowerCase(); const label = query.label.toLowerCase();

View File

@ -2,9 +2,7 @@
<mat-expansion-panel <mat-expansion-panel
*ngFor="let category of queryBuilder.categories" *ngFor="let category of queryBuilder.categories"
[expanded]="category.expanded" [(expanded)]="category.expanded">
(opened)="onCategoryExpanded(category)"
(closed)="onCategoryCollapsed(category)">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ category.name | translate }} {{ category.name | translate }}
@ -17,115 +15,114 @@
</adf-search-widget-container> </adf-search-widget-container>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel *ngIf="isFacetQueriesDefined" [expanded]="facetQueriesExpanded"> <ng-container *ngIf="responseFacetQueries">
<mat-expansion-panel-header> <mat-expansion-panel [expanded]="facetQueriesExpanded">
<mat-panel-title>{{ facetQueriesLabel | translate }}</mat-panel-title> <mat-expansion-panel-header>
</mat-expansion-panel-header> <mat-panel-title>{{ facetQueriesLabel | translate }}</mat-panel-title>
<div class="facet-result-filter"> </mat-expansion-panel-header>
<mat-form-field> <div class="facet-result-filter">
<input <mat-form-field>
matInput <input
placeholder="{{ 'SEARCH.FILTER.ACTIONS.FILTER-CATEGORY' | translate }}" matInput
[(ngModel)]="responseFacetQueries.filterText"> placeholder="{{ 'SEARCH.FILTER.ACTIONS.FILTER-CATEGORY' | translate }}"
<button *ngIf="responseFacetQueries.filterText" [(ngModel)]="responseFacetQueries.filterText">
mat-button matSuffix mat-icon-button <button *ngIf="responseFacetQueries.filterText"
(click)="responseFacetQueries.filterText = ''"> mat-button matSuffix mat-icon-button
<mat-icon>close</mat-icon> (click)="responseFacetQueries.filterText = ''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div class="checklist">
<ng-container *ngFor="let query of responseFacetQueries">
<mat-checkbox
[checked]="query.checked"
(change)="onToggleFacetQuery($event, query)">
{{ query.label }} ({{ query.count }})
</mat-checkbox>
</ng-container>
</div>
<div class="facet-buttons">
<button mat-icon-button
*ngIf="canResetSelectedQueries"
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
(click)="resetSelectedQueries()">
<mat-icon>clear</mat-icon>
</button> </button>
</mat-form-field> <button mat-icon-button
</div> *ngIf="responseFacetQueries.canShowLessItems"
<div class="checklist"> title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}"
<ng-container *ngFor="let query of responseFacetQueries"> (click)="responseFacetQueries.showLessItems()">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
*ngIf="responseFacetQueries.canShowMoreItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}"
(click)="responseFacetQueries.showMoreItems()">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
</ng-container>
<ng-container *ngIf="responseFacetFields">
<mat-expansion-panel *ngFor="let field of responseFacetFields">
<mat-expansion-panel-header>
<mat-panel-title>{{ field.label }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="facet-result-filter">
<mat-form-field>
<input
matInput
placeholder="{{ 'SEARCH.FILTER.ACTIONS.FILTER-CATEGORY' | translate }}"
[(ngModel)]="field.buckets.filterText">
<button *ngIf="field.buckets.filterText"
mat-button matSuffix mat-icon-button
(click)="field.buckets.filterText = ''">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div class="checklist">
<mat-checkbox <mat-checkbox
[checked]="query.$checked" *ngFor="let bucket of field.buckets"
(change)="onFacetQueryToggle($event, query)"> [checked]="bucket.checked"
{{ query.label }} ({{ query.count }}) (change)="onToggleBucket($event, bucket)">
{{ bucket.display || bucket.label }} ({{ bucket.count }})
</mat-checkbox> </mat-checkbox>
</ng-container> </div>
</div>
<div class="facet-buttons">
<button mat-icon-button
*ngIf="canResetSelectedQueries()"
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
(click)="resetSelectedQueries()">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
*ngIf="responseFacetQueries.canShowLessItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}"
(click)="responseFacetQueries.showLessItems()">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
*ngIf="responseFacetQueries.canShowMoreItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}"
(click)="responseFacetQueries.showMoreItems()">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
<mat-expansion-panel <div class="facet-buttons" *ngIf="field.buckets.fitsPage">
*ngFor="let field of responseFacetFields" <button *ngIf="canResetSelectedBuckets(field)"
[expanded]="field.expanded" mat-button
(opened)="onFacetFieldExpanded(field)" color="primary"
(closed)="onFacetFieldCollapsed(field)"> (click)="resetSelectedBuckets(field)">
<mat-expansion-panel-header> {{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
<mat-panel-title>{{ field.label }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="facet-result-filter">
<mat-form-field>
<input
matInput
placeholder="{{ 'SEARCH.FILTER.ACTIONS.FILTER-CATEGORY' | translate }}"
[(ngModel)]="field.buckets.filterText">
<button *ngIf="field.buckets.filterText"
mat-button matSuffix mat-icon-button
(click)="field.buckets.filterText = ''">
<mat-icon>close</mat-icon>
</button> </button>
</mat-form-field> </div>
</div>
<div class="checklist">
<mat-checkbox
*ngFor="let bucket of field.buckets"
[checked]="bucket.$checked"
(change)="onFacetToggle($event, field, bucket)">
{{ bucket.display || bucket.label }} ({{ bucket.count }})
</mat-checkbox>
</div>
<div class="facet-buttons" *ngIf="field.buckets.fitsPage">
<button *ngIf="canResetSelectedBuckets(field)"
mat-button
color="primary"
(click)="resetSelectedBuckets(field)">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
</button>
</div>
<div class="facet-buttons" *ngIf="!field.buckets.fitsPage">
<button mat-icon-button
*ngIf="canResetSelectedBuckets(field)"
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
(click)="resetSelectedBuckets(field)">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
*ngIf="field.buckets.canShowLessItems"
(click)="field.buckets.showLessItems()"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
*ngIf="field.buckets.canShowMoreItems"
(click)="field.buckets.showMoreItems()"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
<div class="facet-buttons" *ngIf="!field.buckets.fitsPage">
<button mat-icon-button
*ngIf="canResetSelectedBuckets(field)"
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
(click)="resetSelectedBuckets(field)">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
*ngIf="field.buckets.canShowLessItems"
(click)="field.buckets.showLessItems()"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
*ngIf="field.buckets.canShowMoreItems"
(click)="field.buckets.showMoreItems()"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
</ng-container>
</mat-accordion> </mat-accordion>

View File

@ -17,15 +17,15 @@
import { SearchFilterComponent } from './search-filter.component'; import { SearchFilterComponent } from './search-filter.component';
import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchConfiguration } from '../../search-configuration.interface';
import { AppConfigService, TranslationMock } from '@alfresco/adf-core'; import { AppConfigService, TranslationMock } from '@alfresco/adf-core';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { ResponseFacetQueryList } from './models/response-facet-query-list.model';
import { ResponseFacetField } from '../../response-facet-field.interface';
import { SearchFilterList } from './models/search-filter-list.model';
import { FacetFieldBucket } from '../../facet-field-bucket.interface'; import { FacetFieldBucket } from '../../facet-field-bucket.interface';
import { FacetQuery } from '../../facet-query.interface';
import { FacetField } from '../../facet-field.interface';
import { SearchFilterList } from './models/search-filter-list.model';
import { ResponseFacetQueryList } from './models/response-facet-query-list.model';
describe('SearchSettingsComponent', () => { describe('SearchFilterComponent', () => {
let component: SearchFilterComponent; let component: SearchFilterComponent;
let queryBuilder: SearchQueryBuilderService; let queryBuilder: SearchQueryBuilderService;
@ -52,150 +52,60 @@ describe('SearchSettingsComponent', () => {
expect(component.onDataLoaded).toHaveBeenCalledWith(data); expect(component.onDataLoaded).toHaveBeenCalledWith(data);
}); });
it('should update category model on expand', () => {
const category: any = { expanded: false };
component.onCategoryExpanded(category);
expect(category.expanded).toBeTruthy();
});
it('should update category model on collapse', () => {
const category: any = { expanded: true };
component.onCategoryCollapsed(category);
expect(category.expanded).toBeFalsy();
});
it('should update facet field model on expand', () => {
const field: any = { expanded: false };
component.onFacetFieldExpanded(field);
expect(field.expanded).toBeTruthy();
});
it('should update facet field model on collapse', () => {
const field: any = { expanded: true };
component.onFacetFieldCollapsed(field);
expect(field.expanded).toBeFalsy();
});
it('should update bucket model and query builder on facet toggle', () => { it('should update bucket model and query builder on facet toggle', () => {
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough();
const event: any = { checked: true }; const event: any = { checked: true };
const field: any = {}; const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 };
const bucket: any = { $checked: false, filterQuery: 'q1' };
component.onFacetToggle(event, field, bucket); component.onToggleBucket(event, bucket);
expect(component.selectedBuckets.length).toBe(1);
expect(component.selectedBuckets[0]).toEqual(bucket);
expect(queryBuilder.filterQueries.length).toBe(1);
expect(queryBuilder.filterQueries[0].query).toBe('q1');
expect(bucket.checked).toBeTruthy();
expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(bucket);
expect(queryBuilder.update).toHaveBeenCalled(); expect(queryBuilder.update).toHaveBeenCalled();
}); });
it('should update bucket model and query builder on facet un-toggle', () => { it('should update bucket model and query builder on facet un-toggle', () => {
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
const event: any = { checked: false }; const event: any = { checked: false };
const field: any = { label: 'f1' }; const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 };
const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' };
component.selectedBuckets.push(bucket); component.onToggleBucket(event, bucket);
queryBuilder.addFilterQuery(bucket.filterQuery);
component.onFacetToggle(event, field, bucket);
expect(bucket.$checked).toBeFalsy();
expect(component.selectedBuckets.length).toBe(0);
expect(queryBuilder.filterQueries.length).toBe(0);
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(bucket);
expect(queryBuilder.update).toHaveBeenCalled(); expect(queryBuilder.update).toHaveBeenCalled();
}); });
it('should unselect facet query and update builder', () => { it('should unselect facet query and update builder', () => {
const translationMock = new TranslationMock();
const config: SearchConfiguration = {
categories: [],
facetQueries: {
queries: [
{ label: 'q1', query: 'query1' }
]
}
};
appConfig.config.search = config;
queryBuilder = new SearchQueryBuilderService(appConfig, null);
component = new SearchFilterComponent(queryBuilder, null, translationMock);
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
queryBuilder.filterQueries = [{ query: 'query1' }]; spyOn(queryBuilder, 'removeUserFacetQuery').and.callThrough();
component.selectedFacetQueries = ['q1'];
component.unselectFacetQuery('q1'); const event: any = { checked: false };
const query: FacetQuery = { checked: true, label: 'q1', query: 'query1' };
expect(component.selectedFacetQueries.length).toBe(0); component.onToggleFacetQuery(event, query);
expect(queryBuilder.filterQueries.length).toBe(0);
expect(query.checked).toBeFalsy();
expect(queryBuilder.removeUserFacetQuery).toHaveBeenCalledWith(query);
expect(queryBuilder.update).toHaveBeenCalled(); expect(queryBuilder.update).toHaveBeenCalled();
}); });
it('should unselect facet bucket and update builder', () => {
spyOn(queryBuilder, 'update').and.stub();
const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' };
component.selectedBuckets.push(bucket);
queryBuilder.filterQueries.push({ query: 'q1' });
component.unselectFacetBucket(bucket);
expect(component.selectedBuckets.length).toBe(0);
expect(queryBuilder.filterQueries.length).toBe(0);
expect(queryBuilder.update).toHaveBeenCalled();
});
it('should allow facetQueries when defined in configuration', () => {
component.queryBuilder.config = {
categories: [],
facetQueries: {
queries: [
{ label: 'q1', query: 'query1' }
]
}
};
expect(component.isFacetQueriesDefined).toBe(true);
});
it('should not allow facetQueries when not defined in configuration', () => {
component.queryBuilder.config = {
categories: []
};
expect(component.isFacetQueriesDefined).toBe(false);
});
it('should not allow facetQueries when queries are not defined in configuration', () => {
component.queryBuilder.config = {
categories: [],
facetQueries: {
queries: []
}
};
expect(component.isFacetQueriesDefined).toBe(false);
});
it('should fetch facet queries from response payload', () => { it('should fetch facet queries from response payload', () => {
component.responseFacetQueries = new ResponseFacetQueryList(); component.responseFacetQueries = null;
queryBuilder.config = {
categories: [],
facetQueries: {
queries: [
{ label: 'q1', query: 'query1' },
{ label: 'q2', query: 'query2' }
]
}
};
const queries = [ const queries = [
{ label: 'q1', query: 'query1', count: 1 }, { label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2', count: 1 } { label: 'q2', query: 'query2', count: 1 }
@ -214,8 +124,51 @@ describe('SearchSettingsComponent', () => {
expect(component.responseFacetQueries.items).toEqual(queries); expect(component.responseFacetQueries.items).toEqual(queries);
}); });
it('should preserve order after response processing', () => {
component.responseFacetQueries = null;
queryBuilder.config = {
categories: [],
facetQueries: {
queries: [
{ label: 'q1', query: 'query1' },
{ label: 'q2', query: 'query2' },
{ label: 'q3', query: 'query3' }
]
}
};
const queries = [
{ label: 'q2', query: 'query2', count: 1 },
{ label: 'q1', query: 'query1', count: 1 },
{ label: 'q3', query: 'query3', count: 1 }
];
const data = {
list: {
context: {
facetQueries: queries
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(3);
expect(component.responseFacetQueries.items[0].label).toBe('q1');
expect(component.responseFacetQueries.items[1].label).toBe('q2');
expect(component.responseFacetQueries.items[2].label).toBe('q3');
});
it('should not fetch facet queries from response payload', () => { it('should not fetch facet queries from response payload', () => {
component.responseFacetQueries = new ResponseFacetQueryList(); component.responseFacetQueries = null;
queryBuilder.config = {
categories: [],
facetQueries: {
queries: []
}
};
const data = { const data = {
list: { list: {
@ -227,57 +180,22 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data); component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(0); expect(component.responseFacetQueries).toBeNull();
});
it('should restore checked state for new response facet queries', () => {
component.selectedFacetQueries = ['q3'];
component.responseFacetQueries = new ResponseFacetQueryList();
const queries = [
{ label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2', count: 1 }
];
const data = {
list: {
context: {
facetQueries: queries
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(2);
expect((<any> component.responseFacetQueries.items[0]).$checked).toBeFalsy();
expect((<any> component.responseFacetQueries.items[1]).$checked).toBeFalsy();
});
it('should not restore checked state for new response facet queries', () => {
component.selectedFacetQueries = ['q2'];
component.responseFacetQueries = new ResponseFacetQueryList();
const queries = [
{ label: 'q1', query: 'query1', count: 1 },
{ label: 'q2', query: 'query2', count: 1 }
];
const data = {
list: {
context: {
facetQueries: queries
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(2);
expect((<any> component.responseFacetQueries.items[0]).$checked).toBeFalsy();
expect((<any> component.responseFacetQueries.items[1]).$checked).toBeTruthy();
}); });
it('should fetch facet fields from response payload', () => { it('should fetch facet fields from response payload', () => {
component.responseFacetFields = []; component.responseFacetFields = null;
queryBuilder.config = {
categories: [],
facetFields: [
{ label: 'f1', field: 'f1' },
{ label: 'f2', field: 'f2' }
],
facetQueries: {
queries: []
}
};
const fields: any = [ const fields: any = [
{ label: 'f1', buckets: [] }, { label: 'f1', buckets: [] },
@ -293,95 +211,25 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data); component.onDataLoaded(data);
expect(component.responseFacetFields).toEqual(fields); expect(component.responseFacetFields.length).toEqual(2);
});
it('should restore expanded state for new response facet fields', () => {
component.responseFacetFields = <any> [
{ label: 'f1', buckets: [] },
{ label: 'f2', buckets: [], expanded: true }
];
const fields = [
{ label: 'f1', buckets: [] },
{ label: 'f2', buckets: [] }
];
const data = {
list: {
context: {
facetsFields: fields
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetFields.length).toBe(2);
expect(component.responseFacetFields[0].expanded).toBeFalsy();
expect(component.responseFacetFields[1].expanded).toBeTruthy();
});
it('should restore checked buckets for new response facet fields', () => {
const bucket1 = { label: 'b1', $field: 'f1', count: 1, filterQuery: 'q1' };
const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' };
component.selectedBuckets = [bucket2];
component.responseFacetFields = <any> [
{ label: 'f2', buckets: [] }
];
const data = {
list: {
context: {
facetsFields: [
{ label: 'f1', buckets: [bucket1] },
{ label: 'f2', buckets: [bucket2] }
]
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetFields.length).toBe(2);
expect(component.responseFacetFields[0].buckets.items[0].$checked).toBeFalsy();
expect(component.responseFacetFields[1].buckets.items[0].$checked).toBeTruthy();
});
it('should reset queries and fields on empty response payload', () => {
component.responseFacetQueries = new ResponseFacetQueryList([<any> {}, <any> {}]);
component.responseFacetFields = [<any> {}, <any> {}];
const data = {
list: {
context: {
facetQueries: null,
facetsFields: null
}
}
};
component.onDataLoaded(data);
expect(component.responseFacetQueries.length).toBe(0);
expect(component.responseFacetFields.length).toBe(0);
}); });
it('should update query builder only when has bucket to unselect', () => { it('should update query builder only when has bucket to unselect', () => {
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
component.unselectFacetBucket(null); component.onToggleBucket(<any> { checked: true }, null);
expect(queryBuilder.update).not.toHaveBeenCalled(); expect(queryBuilder.update).not.toHaveBeenCalled();
}); });
it('should allow to to reset selected buckets', () => { it('should allow to to reset selected buckets', () => {
const buckets: FacetFieldBucket[] = [ const buckets: FacetFieldBucket[] = [
{ label: 'bucket1', $checked: true, count: 1, filterQuery: 'q1' }, { label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' },
{ label: 'bucket2', $checked: false, count: 1, filterQuery: 'q2' } { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
]; ];
const field: ResponseFacetField = { const field: FacetField = {
field: 'f1',
label: 'field1', label: 'field1',
buckets: new SearchFilterList<FacetFieldBucket>(buckets) buckets: new SearchFilterList<FacetFieldBucket>(buckets)
}; };
@ -391,11 +239,12 @@ describe('SearchSettingsComponent', () => {
it('should not allow to reset selected buckets', () => { it('should not allow to reset selected buckets', () => {
const buckets: FacetFieldBucket[] = [ const buckets: FacetFieldBucket[] = [
{ label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1' }, { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
{ label: 'bucket2', $checked: false, count: 1, filterQuery: 'q2' } { label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
]; ];
const field: ResponseFacetField = { const field: FacetField = {
field: 'f1',
label: 'field1', label: 'field1',
buckets: new SearchFilterList<FacetFieldBucket>(buckets) buckets: new SearchFilterList<FacetFieldBucket>(buckets)
}; };
@ -405,70 +254,57 @@ describe('SearchSettingsComponent', () => {
it('should reset selected buckets', () => { it('should reset selected buckets', () => {
const buckets: FacetFieldBucket[] = [ const buckets: FacetFieldBucket[] = [
{ label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1', $field: 'field1' }, { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
{ label: 'bucket2', $checked: true, count: 1, filterQuery: 'q2', $field: 'field1' } { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
]; ];
const field: ResponseFacetField = { const field: FacetField = {
field: 'f1',
label: 'field1', label: 'field1',
buckets: new SearchFilterList<FacetFieldBucket>(buckets) buckets: new SearchFilterList<FacetFieldBucket>(buckets)
}; };
component.selectedBuckets = [buckets[1]];
component.resetSelectedBuckets(field); component.resetSelectedBuckets(field);
expect(buckets[0].$checked).toBeFalsy(); expect(buckets[0].checked).toBeFalsy();
expect(buckets[1].$checked).toBeFalsy(); expect(buckets[1].checked).toBeFalsy();
expect(component.selectedBuckets.length).toBe(0);
}); });
it('should update query builder upon resetting buckets', () => { it('should update query builder upon resetting buckets', () => {
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
const buckets: FacetFieldBucket[] = [ const buckets: FacetFieldBucket[] = [
{ label: 'bucket1', $checked: false, count: 1, filterQuery: 'q1', $field: 'field1' }, { label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
{ label: 'bucket2', $checked: true, count: 1, filterQuery: 'q2', $field: 'field1' } { label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
]; ];
const field: ResponseFacetField = { const field: FacetField = {
field: 'f1',
label: 'field1', label: 'field1',
buckets: new SearchFilterList<FacetFieldBucket>(buckets) buckets: new SearchFilterList<FacetFieldBucket>(buckets)
}; };
component.selectedBuckets = [buckets[1]];
component.resetSelectedBuckets(field); component.resetSelectedBuckets(field);
expect(queryBuilder.update).toHaveBeenCalled(); expect(queryBuilder.update).toHaveBeenCalled();
}); });
it('should allow to reset selected queries', () => {
component.selectedFacetQueries = ['q1', 'q2'];
expect(component.canResetSelectedQueries()).toBeTruthy();
});
it('should not allow to reset selected queries when nothing selected', () => {
component.selectedFacetQueries = [];
expect(component.canResetSelectedQueries()).toBeFalsy();
});
it('should reset selected queries', () => {
const methodSpy = spyOn(component, 'unselectFacetQuery').and.stub();
component.selectedFacetQueries = ['q1', 'q2'];
component.resetSelectedQueries();
expect(methodSpy.calls.count()).toBe(2);
expect(methodSpy.calls.argsFor(0)).toEqual(['q1', false]);
expect(methodSpy.calls.argsFor(1)).toEqual(['q2', false]);
});
it('should update query builder upon resetting selected queries', () => { it('should update query builder upon resetting selected queries', () => {
spyOn(queryBuilder, 'update').and.stub(); spyOn(queryBuilder, 'update').and.stub();
spyOn(queryBuilder, 'removeUserFacetQuery').and.callThrough();
component.selectedFacetQueries = ['q1', 'q2']; component.canResetSelectedQueries = true;
component.responseFacetQueries = new ResponseFacetQueryList([
{ label: 'q1', query: 'q1', checked: true, count: 1 },
{ label: 'q2', query: 'q2', checked: false, count: 1 },
{ label: 'q3', query: 'q3', checked: true, count: 1 }
]);
component.resetSelectedQueries(); component.resetSelectedQueries();
expect(queryBuilder.removeUserFacetQuery).toHaveBeenCalledTimes(3);
expect(queryBuilder.update).toHaveBeenCalled(); expect(queryBuilder.update).toHaveBeenCalled();
});
for (let entry of component.responseFacetQueries.items) {
expect(entry.checked).toBeFalsy();
}
});
}); });

View File

@ -19,11 +19,10 @@ import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core';
import { MatCheckboxChange } from '@angular/material'; import { MatCheckboxChange } from '@angular/material';
import { SearchService, TranslationService } from '@alfresco/adf-core'; import { SearchService, TranslationService } from '@alfresco/adf-core';
import { SearchQueryBuilderService } from '../../search-query-builder.service'; import { SearchQueryBuilderService } from '../../search-query-builder.service';
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 { ResponseFacetQuery } from '../../response-facet-query.interface';
import { ResponseFacetQueryList } from './models/response-facet-query-list.model'; import { ResponseFacetQueryList } from './models/response-facet-query-list.model';
import { FacetQuery } from '../../facet-query.interface';
import { FacetField } from '../../facet-field.interface';
import { SearchFilterList } from './models/search-filter-list.model'; import { SearchFilterList } from './models/search-filter-list.model';
@Component({ @Component({
@ -38,20 +37,20 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private DEFAULT_PAGE_SIZE = 5; private DEFAULT_PAGE_SIZE = 5;
isAlive = true; isAlive = true;
selectedFacetQueries: string[] = []; responseFacetQueries: ResponseFacetQueryList = null;
selectedBuckets: FacetFieldBucket[] = []; responseFacetFields: FacetField[] = null;
responseFacetQueries: ResponseFacetQueryList;
responseFacetFields: ResponseFacetField[] = [];
private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
facetQueriesLabel: string = 'Facet Queries'; facetQueriesLabel: string = 'Facet Queries';
facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
facetQueriesExpanded = false; facetQueriesExpanded = false;
canResetSelectedQueries = false;
selectedFacetQueries: Array<FacetQuery> = [];
selectedBuckets: Array<FacetFieldBucket> = [];
constructor(public queryBuilder: SearchQueryBuilderService, constructor(public queryBuilder: SearchQueryBuilderService,
private searchService: SearchService, private searchService: SearchService,
private translationService: TranslationService) { private translationService: TranslationService) {
this.responseFacetQueries = new ResponseFacetQueryList();
if (queryBuilder.config && queryBuilder.config.facetQueries) { if (queryBuilder.config && queryBuilder.config.facetQueries) {
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries'; this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE; this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE;
@ -60,9 +59,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
this.queryBuilder.updated this.queryBuilder.updated
.takeWhile(() => this.isAlive) .takeWhile(() => this.isAlive)
.subscribe(query => { .subscribe(() => {
this.queryBuilder.execute(); this.queryBuilder.execute();
}); });
} }
ngOnInit() { ngOnInit() {
@ -70,9 +69,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
this.queryBuilder.executed this.queryBuilder.executed
.takeWhile(() => this.isAlive) .takeWhile(() => this.isAlive)
.subscribe(data => { .subscribe(data => {
this.onDataLoaded(data); this.onDataLoaded(data);
this.searchService.dataLoaded.next(data); this.searchService.dataLoaded.next(data);
}); });
} }
} }
@ -80,125 +79,111 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
this.isAlive = false; this.isAlive = false;
} }
get isFacetQueriesDefined() { onToggleFacetQuery(event: MatCheckboxChange, facetQuery: FacetQuery) {
return this.queryBuilder.hasFacetQueries; if (event && facetQuery) {
} if (event.checked) {
this.selectFacetQuery(facetQuery);
onCategoryExpanded(category: SearchCategory) { } else {
category.expanded = true; this.unselectFacetQuery(facetQuery);
}
onCategoryCollapsed(category: SearchCategory) {
category.expanded = false;
}
onFacetFieldExpanded(field: ResponseFacetField) {
field.expanded = true;
}
onFacetFieldCollapsed(field: ResponseFacetField) {
field.expanded = false;
}
onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) {
const facetQuery = this.queryBuilder.getFacetQuery(query.label);
if (event.checked) {
query.$checked = true;
this.selectedFacetQueries.push(facetQuery.label);
if (facetQuery) {
this.queryBuilder.addFilterQuery(facetQuery.query);
}
} else {
query.$checked = false;
this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== query.label);
if (facetQuery) {
this.queryBuilder.removeFilterQuery(facetQuery.query);
} }
} }
this.queryBuilder.update();
} }
onFacetToggle(event: MatCheckboxChange, field: ResponseFacetField, bucket: FacetFieldBucket) { selectFacetQuery(query: FacetQuery) {
if (event.checked) { if (query) {
bucket.$checked = true; query.checked = true;
this.selectedBuckets.push({ ...bucket }); this.queryBuilder.addUserFacetQuery(query);
this.queryBuilder.addFilterQuery(bucket.filterQuery); this.updateSelectedFields();
} else {
bucket.$checked = false;
const idx = this.selectedBuckets.findIndex(
b => b.$field === bucket.$field && b.label === bucket.label
);
if (idx >= 0) {
this.selectedBuckets.splice(idx, 1);
}
this.queryBuilder.removeFilterQuery(bucket.filterQuery);
}
this.queryBuilder.update();
}
unselectFacetQuery(label: string, reloadQuery: boolean = true) {
const facetQuery = this.queryBuilder.getFacetQuery(label);
if (facetQuery) {
this.queryBuilder.removeFilterQuery(facetQuery.query);
}
this.selectedFacetQueries = this.selectedFacetQueries.filter(selectedQuery => selectedQuery !== label);
if (reloadQuery) {
this.queryBuilder.update(); this.queryBuilder.update();
} }
} }
unselectFacetBucket(bucket: FacetFieldBucket, reloadQuery: boolean = true) { unselectFacetQuery(query: FacetQuery) {
if (bucket) { if (query) {
const idx = this.selectedBuckets.findIndex( query.checked = false;
selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label this.queryBuilder.removeUserFacetQuery(query);
); this.updateSelectedFields();
this.queryBuilder.update();
}
}
if (idx >= 0) { private updateSelectedBuckets() {
this.selectedBuckets.splice(idx, 1); if (this.responseFacetFields) {
this.selectedBuckets = [];
for (let field of this.responseFacetFields) {
if (field.buckets) {
this.selectedBuckets.push(...field.buckets.items.filter(bucket => bucket.checked));
}
} }
this.queryBuilder.removeFilterQuery(bucket.filterQuery); } else {
this.selectedBuckets = [];
}
}
bucket.$checked = false; private updateSelectedFields() {
if (this.responseFacetQueries) {
this.selectedFacetQueries = this.responseFacetQueries.items.filter(item => item.checked);
this.canResetSelectedQueries = this.selectedFacetQueries.length > 0;
} else {
this.selectedFacetQueries = [];
this.canResetSelectedQueries = false;
}
}
if (reloadQuery) { onToggleBucket(event: MatCheckboxChange, bucket: FacetFieldBucket) {
this.queryBuilder.update(); if (event && bucket) {
if (event.checked) {
this.selectFacetBucket(bucket);
} else {
this.unselectFacetBucket(bucket);
} }
} }
} }
canResetSelectedQueries(): boolean { selectFacetBucket(bucket: FacetFieldBucket) {
return this.selectedFacetQueries && this.selectedFacetQueries.length > 0; if (bucket) {
bucket.checked = true;
this.queryBuilder.addUserFacetBucket(bucket);
this.updateSelectedBuckets();
this.queryBuilder.update();
}
}
unselectFacetBucket(bucket: FacetFieldBucket) {
if (bucket) {
bucket.checked = false;
this.queryBuilder.removeUserFacetBucket(bucket);
this.updateSelectedBuckets();
this.queryBuilder.update();
}
} }
resetSelectedQueries() { resetSelectedQueries() {
if (this.canResetSelectedQueries()) { if (this.canResetSelectedQueries) {
this.selectedFacetQueries.forEach(query => { for (let query of this.responseFacetQueries.items) {
this.unselectFacetQuery(query, false); query.checked = false;
}); this.queryBuilder.removeUserFacetQuery(query);
}
this.selectedFacetQueries = [];
this.canResetSelectedQueries = false;
this.queryBuilder.update(); this.queryBuilder.update();
} }
} }
canResetSelectedBuckets(field: ResponseFacetField): boolean { canResetSelectedBuckets(field: FacetField): boolean {
if (field && field.buckets) { if (field && field.buckets) {
return field.buckets.items.some(bucket => bucket.$checked); return field.buckets.items.some(bucket => bucket.checked);
} }
return false; return false;
} }
resetSelectedBuckets(field: ResponseFacetField) { resetSelectedBuckets(field: FacetField) {
if (field && field.buckets) { if (field && field.buckets) {
field.buckets.items.forEach(bucket => { for (let bucket of field.buckets.items) {
this.unselectFacetBucket(bucket, false); bucket.checked = false;
}); this.queryBuilder.removeUserFacetBucket(bucket);
}
this.updateSelectedBuckets();
this.queryBuilder.update(); this.queryBuilder.update();
} }
} }
@ -207,64 +192,75 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
const context = data.list.context; const context = data.list.context;
if (context) { if (context) {
const facetQueries = (context.facetQueries || []).map(query => { this.parseFacetFields(context);
query.label = this.translationService.instant(query.label); this.parseFacetQueries(context);
query.$checked = this.selectedFacetQueries.includes(query.label);
return query;
});
this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize);
const expandedFields = this.responseFacetFields
.filter(field => field.expanded)
.map(field => field.label);
this.responseFacetFields = (context.facetsFields || []).map(
field => {
const settings = this.queryBuilder.getFacetField(field.label);
let fallbackPageSize = this.DEFAULT_PAGE_SIZE;
if (settings && settings.pageSize) {
fallbackPageSize = settings.pageSize;
}
field.label = this.translationService.instant(field.label);
field.pageSize = field.pageSize || fallbackPageSize;
field.currentPageSize = field.pageSize;
field.expanded = expandedFields.includes(field.label);
const buckets = (field.buckets || []).map(bucket => {
bucket.$field = field.label;
bucket.$checked = false;
bucket.display = this.translationService.instant(bucket.display);
bucket.label = this.translationService.instant(bucket.label);
const previousBucket = this.selectedBuckets.find(
selectedBucket => selectedBucket.$field === bucket.$field && selectedBucket.label === bucket.label
);
if (previousBucket) {
bucket.$checked = true;
}
return bucket;
});
const bucketList = new SearchFilterList<FacetFieldBucket>(buckets, field.pageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => {
if (bucket && bucketList.filterText) {
const pattern = (bucketList.filterText || '').toLowerCase();
const label = (bucket.display || bucket.label || '').toLowerCase();
return label.startsWith(pattern);
}
return true;
};
field.buckets = bucketList;
return field;
}
);
} else { } else {
this.responseFacetQueries = new ResponseFacetQueryList([], this.facetQueriesPageSize); this.responseFacetQueries = null;
this.responseFacetFields = []; this.responseFacetFields = null;
} }
} }
private parseFacetFields(context: any) {
if (!this.responseFacetFields) {
const configFacetFields = this.queryBuilder.config.facetFields || [];
this.responseFacetFields = configFacetFields.map(field => {
const responseField = (context.facetsFields || []).find(response => response.label === field.label);
const buckets: FacetFieldBucket[] = (responseField.buckets || []).map(bucket => {
return <FacetFieldBucket> {
...bucket,
checked: false,
display: this.translationService.instant(bucket.display),
label: this.translationService.instant(bucket.label)
};
});
const bucketList = new SearchFilterList<FacetFieldBucket>(buckets, field.pageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => {
if (bucket && bucketList.filterText) {
const pattern = (bucketList.filterText || '').toLowerCase();
const label = (bucket.display || bucket.label || '').toLowerCase();
return label.startsWith(pattern);
}
return true;
};
return {
...field,
label: this.translationService.instant(field.label),
pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
buckets: bucketList
};
});
}
}
private parseFacetQueries(context: any) {
const responseQueries = this.getFacetQueryMap(context);
if (!this.responseFacetQueries) {
const facetQueries = (this.queryBuilder.config.facetQueries.queries || [])
.map(query => {
const queryResult = responseQueries[query.label];
return <FacetQuery> {
...query,
label: this.translationService.instant(query.label),
count: queryResult.count
};
});
if (facetQueries.length > 0) {
this.responseFacetQueries = new ResponseFacetQueryList(facetQueries, this.facetQueriesPageSize);
} else {
this.responseFacetQueries = null;
}
}
}
private getFacetQueryMap(context: any): { [key: string]: any } {
const result = {};
(context.facetQueries || []).forEach(query => {
result[query.label] = query;
});
return result;
}
} }

View File

@ -21,6 +21,5 @@ export interface FacetFieldBucket {
label: string; label: string;
filterQuery: string; filterQuery: string;
$checked?: boolean; checked?: boolean;
$field?: string;
} }

View File

@ -15,6 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { SearchFilterList } from './components/search-filter/models/search-filter-list.model';
import { FacetFieldBucket } from './facet-field-bucket.interface';
export interface FacetField { export interface FacetField {
field: string; field: string;
label: string; label: string;
@ -23,6 +26,8 @@ export interface FacetField {
offset?: number; offset?: number;
prefix?: string; prefix?: string;
buckets?: SearchFilterList<FacetFieldBucket>;
pageSize?: number; pageSize?: number;
$checked?: boolean; currentPageSize?: number;
checked?: boolean;
} }

View File

@ -16,12 +16,9 @@
*/ */
export interface FacetQuery { export interface FacetQuery {
query: string;
label: string; label: string;
} query: string;
export interface ResponseFacetQuery { checked?: boolean;
label?: string;
filterQuery?: string;
count?: number; count?: number;
} }

View File

@ -19,8 +19,6 @@ export { FacetFieldBucket } from './facet-field-bucket.interface';
export { FacetField } from './facet-field.interface'; export { FacetField } from './facet-field.interface';
export { FacetQuery } from './facet-query.interface'; export { FacetQuery } from './facet-query.interface';
export { FilterQuery } from './filter-query.interface'; export { FilterQuery } from './filter-query.interface';
export { ResponseFacetField } from './response-facet-field.interface';
export { ResponseFacetQuery } from './response-facet-query.interface';
export { SearchCategory } from './search-category.interface'; export { SearchCategory } from './search-category.interface';
export { SearchWidgetSettings } from './search-widget-settings.interface'; export { SearchWidgetSettings } from './search-widget-settings.interface';
export { SearchWidget } from './search-widget.interface'; export { SearchWidget } from './search-widget.interface';

View File

@ -1,27 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FacetFieldBucket } from './facet-field-bucket.interface';
import { SearchFilterList } from './components/search-filter/models/search-filter-list.model';
export interface ResponseFacetField {
label: string;
buckets: SearchFilterList<FacetFieldBucket>;
pageSize?: number;
currentPageSize?: number;
expanded?: boolean;
}

View File

@ -1,23 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface ResponseFacetQuery {
label: string;
mincount: number;
$checked?: boolean;
}

View File

@ -212,7 +212,7 @@ describe('SearchQueryBuilder', () => {
const builder = new SearchQueryBuilderService(buildConfig(config), null); const builder = new SearchQueryBuilderService(buildConfig(config), null);
const field = builder.getFacetField('Missing'); const field = builder.getFacetField('Missing');
expect(field).toBeUndefined(); expect(field).toBeFalsy();
}); });
xit('should build query and raise an event on update', async () => { xit('should build query and raise an event on update', async () => {

View File

@ -26,6 +26,7 @@ import { SearchConfiguration } from './search-configuration.interface';
import { FacetQuery } from './facet-query.interface'; import { FacetQuery } from './facet-query.interface';
import { SearchSortingDefinition } from './search-sorting-definition.interface'; import { SearchSortingDefinition } from './search-sorting-definition.interface';
import { FacetField } from './facet-field.interface'; import { FacetField } from './facet-field.interface';
import { FacetFieldBucket } from './facet-field-bucket.interface';
@Injectable() @Injectable()
export class SearchQueryBuilderService { export class SearchQueryBuilderService {
@ -41,6 +42,9 @@ export class SearchQueryBuilderService {
paging: { maxItems?: number; skipCount?: number } = null; paging: { maxItems?: number; skipCount?: number } = null;
sorting: Array<SearchSortingDefinition> = []; sorting: Array<SearchSortingDefinition> = [];
protected userFacetQueries: FacetQuery[] = [];
protected userFacetBuckets: FacetFieldBucket[] = [];
get userQuery(): string { get userQuery(): string {
return this._userQuery; return this._userQuery;
} }
@ -65,12 +69,48 @@ export class SearchQueryBuilderService {
this.config = JSON.parse(JSON.stringify(template)); this.config = JSON.parse(JSON.stringify(template));
this.categories = (this.config.categories || []).filter(category => category.enabled); this.categories = (this.config.categories || []).filter(category => category.enabled);
this.filterQueries = this.config.filterQueries || []; this.filterQueries = this.config.filterQueries || [];
this.userFacetBuckets = [];
this.userFacetQueries = [];
if (this.config.sorting) { if (this.config.sorting) {
this.sorting = this.config.sorting.defaults || []; this.sorting = this.config.sorting.defaults || [];
} }
} }
} }
addUserFacetQuery(query: FacetQuery) {
if (query) {
const existing = this.userFacetQueries.find(facetQuery => facetQuery.label === query.label);
if (existing) {
existing.query = query.query;
} else {
this.userFacetQueries.push({ ...query });
}
}
}
removeUserFacetQuery(query: FacetQuery) {
if (query) {
this.userFacetQueries = this.userFacetQueries
.filter(facetQuery => facetQuery.label !== query.label);
}
}
addUserFacetBucket(bucket: FacetFieldBucket) {
if (bucket) {
const existing = this.userFacetBuckets.find(facetBucket => facetBucket.label === bucket.label);
if (!existing) {
this.userFacetBuckets.push(bucket);
}
}
}
removeUserFacetBucket(bucket: FacetFieldBucket) {
if (bucket) {
this.userFacetBuckets = this.userFacetBuckets
.filter(facetBucket => facetBucket.label !== bucket.label);
}
}
addFilterQuery(query: string): void { addFilterQuery(query: string): void {
if (query) { if (query) {
const existing = this.filterQueries.find(filterQuery => filterQuery.query === query); const existing = this.filterQueries.find(filterQuery => filterQuery.query === query);
@ -89,7 +129,10 @@ export class SearchQueryBuilderService {
getFacetQuery(label: string): FacetQuery { getFacetQuery(label: string): FacetQuery {
if (label && this.hasFacetQueries) { if (label && this.hasFacetQueries) {
return this.config.facetQueries.queries.find(query => query.label === label); const result = this.config.facetQueries.queries.find(query => query.label === label);
if (result) {
return { ...result };
}
} }
return null; return null;
} }
@ -97,7 +140,10 @@ export class SearchQueryBuilderService {
getFacetField(label: string): FacetField { getFacetField(label: string): FacetField {
if (label) { if (label) {
const fields = this.config.facetFields || []; const fields = this.config.facetFields || [];
return fields.find(field => field.label === label); const result = fields.find(field => field.label === label);
if (result) {
return { ...result };
}
} }
return null; return null;
} }
@ -175,7 +221,7 @@ export class SearchQueryBuilderService {
return false; return false;
} }
private get sort(): RequestSortDefinitionInner[] { protected get sort(): RequestSortDefinitionInner[] {
return this.sorting.map(def => { return this.sorting.map(def => {
return { return {
type: def.type, type: def.type,
@ -185,7 +231,7 @@ export class SearchQueryBuilderService {
}); });
} }
private get facetQueries(): FacetQuery[] { protected get facetQueries(): FacetQuery[] {
if (this.hasFacetQueries) { if (this.hasFacetQueries) {
return this.config.facetQueries.queries.map(query => { return this.config.facetQueries.queries.map(query => {
return <FacetQuery> { ...query }; return <FacetQuery> { ...query };
@ -195,7 +241,7 @@ export class SearchQueryBuilderService {
return null; return null;
} }
private getFinalQuery(): string { protected getFinalQuery(): string {
let query = ''; let query = '';
this.categories.forEach(facet => { this.categories.forEach(facet => {
@ -208,14 +254,28 @@ export class SearchQueryBuilderService {
} }
}); });
const result = [this.userQuery, query] let result = [this.userQuery, query]
.filter(entry => entry) .filter(entry => entry)
.join(' AND '); .join(' AND ');
if (this.userFacetQueries && this.userFacetQueries.length > 0) {
const combined = this.userFacetQueries
.map(userQuery => userQuery.query)
.join(' OR ');
result += ` AND (${combined})`;
}
if (this.userFacetBuckets && this.userFacetBuckets.length > 0) {
const combined = this.userFacetBuckets
.map(bucket => bucket.filterQuery)
.join(' OR ');
result += ` AND (${combined})`;
}
return result; return result;
} }
private get facetFields(): RequestFacetFields { protected get facetFields(): RequestFacetFields {
const facetFields = this.config.facetFields; const facetFields = this.config.facetFields;
if (facetFields && facetFields.length > 0) { if (facetFields && facetFields.length > 0) {

View File

@ -5031,22 +5031,6 @@
}, },
"name": "RequiredFieldValidator" "name": "RequiredFieldValidator"
}, },
{
"position": {
"line": 21,
"character": 9,
"fileName": "lib/content-services/search/public-api.ts"
},
"name": "ResponseFacetField"
},
{
"position": {
"line": 22,
"character": 9,
"fileName": "lib/content-services/search/public-api.ts"
},
"name": "ResponseFacetQuery"
},
{ {
"position": { "position": {
"line": 25, "line": 25,