search fixes (#3272)

* "show less" button for search filter container

* consistent button styles across widgets

* i18n support for facets

* page sizes for check list

* display page buttons only when needed

* page sizes for all facet fields

* test fixes

* update lib versions

* fix angular configuration
This commit is contained in:
Denys Vuika
2018-05-08 13:41:27 +01:00
committed by Eugenio Romano
parent 87a464907c
commit ba35eda2f9
25 changed files with 518 additions and 258 deletions

View File

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

View File

@@ -5,8 +5,28 @@
{{ option.name | translate }}
</mat-checkbox>
<div>
<div class="facet-buttons" *ngIf="options.fitsPage">
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
</button>
</div>
<div class="facet-buttons" *ngIf="!options.fitsPage">
<button mat-icon-button
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
(click)="reset()">
<mat-icon>clear</mat-icon>
</button>
<button mat-icon-button
[disabled]="!options.canShowLessItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}"
(click)="options.showLessItems()">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
[disabled]="!options.canShowMoreItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}"
(click)="options.showMoreItems()">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>

View File

@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { SearchCheckListComponent } from './search-check-list.component';
import { SearchCheckListComponent, SearchListOption } from './search-check-list.component';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
describe('SearchCheckListComponent', () => {
@@ -33,7 +34,7 @@ describe('SearchCheckListComponent', () => {
component.settings = <any> { options: options };
component.ngOnInit();
expect(component.options).toEqual(options);
expect(component.options.items).toEqual(options);
});
it('should setup operator from the settings', () => {
@@ -49,10 +50,10 @@ describe('SearchCheckListComponent', () => {
});
it('should update query builder on checkbox change', () => {
component.options = [
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: false },
{ name: 'Document', value: "TYPE:'cm:content'", checked: false }
];
]);
component.id = 'checklist';
component.context = <any> {
@@ -66,14 +67,14 @@ describe('SearchCheckListComponent', () => {
component.changeHandler(
<any> { checked: true },
component.options[0]
component.options.items[0]
);
expect(component.context.queryFragments[component.id]).toEqual(`TYPE:'cm:folder'`);
component.changeHandler(
<any> { checked: true },
component.options[1]
component.options.items[1]
);
expect(component.context.queryFragments[component.id]).toEqual(
@@ -82,15 +83,15 @@ describe('SearchCheckListComponent', () => {
});
it('should reset selected boxes', () => {
component.options = [
component.options = new SearchFilterList<SearchListOption>([
{ 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();
expect(component.options.items[0].checked).toBeFalsy();
expect(component.options.items[1].checked).toBeFalsy();
});
it('should update query builder on reset', () => {
@@ -104,10 +105,10 @@ describe('SearchCheckListComponent', () => {
spyOn(component.context, 'update').and.stub();
component.ngOnInit();
component.options = [
component.options = new SearchFilterList<SearchListOption>([
{ name: 'Folder', value: "TYPE:'cm:folder'", checked: true },
{ name: 'Document', value: "TYPE:'cm:content'", checked: true }
];
]);
component.reset();

View File

@@ -20,6 +20,13 @@ 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';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
export interface SearchListOption {
name: string;
value: string;
checked: boolean;
}
@Component({
selector: 'adf-search-check-list',
@@ -33,21 +40,27 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
id: string;
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
options: { name: string, value: string, checked: boolean }[] = [];
options: SearchFilterList<SearchListOption>;
operator: string = 'OR';
pageSize = 5;
constructor() {
this.options = new SearchFilterList<SearchListOption>();
}
ngOnInit(): void {
if (this.settings) {
this.operator = this.settings.operator || 'OR';
this.pageSize = this.settings.pageSize || 5;
if (this.settings.options && this.settings.options.length > 0) {
this.options = [...this.settings.options];
this.options = new SearchFilterList(this.settings.options, this.pageSize);
}
}
}
reset() {
this.options.forEach(opt => {
this.options.items.forEach(opt => {
opt.checked = false;
});
@@ -63,7 +76,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
}
flush() {
const checkedValues = this.options
const checkedValues = this.options.items
.filter(option => option.checked)
.map(option => option.value);

View File

@@ -4,7 +4,7 @@
*ngFor="let label of searchFilter.selectedFacetQueries"
[removable]="true"
(remove)="searchFilter.unselectFacetQuery(label)">
{{ label }}
{{ label | translate }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</ng-container>
@@ -13,7 +13,7 @@
*ngFor="let bucket of searchFilter.selectedBuckets"
[removable]="true"
(remove)="searchFilter.unselectFacetBucket(bucket)">
{{ bucket.display || bucket.label }}
{{ (bucket.display || bucket.label) | translate }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</ng-container>

View File

@@ -1,36 +1,34 @@
<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]="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>
<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 class="facet-buttons">
<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

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

View File

@@ -16,43 +16,20 @@
*/
import { ResponseFacetQuery } from '../../../facet-query.interface';
import { SearchFilterList } from './search-filter-list.model';
export class ResponseFacetQueryList {
items: ResponseFacetQuery[] = [];
pageSize: number = 5;
currentPageSize: number = 5;
get visibleItems(): ResponseFacetQuery[] {
return this.items.slice(0, this.currentPageSize);
}
get length(): number {
return this.items.length;
}
export class ResponseFacetQueryList extends SearchFilterList<ResponseFacetQuery> {
constructor(items: ResponseFacetQuery[] = [], pageSize: number = 5) {
this.items = items
const filtered = items
.filter(item => {
return item.count > 0;
})
.map(item => {
return <ResponseFacetQuery> { ...item };
});
this.pageSize = pageSize;
this.currentPageSize = pageSize;
super(filtered, pageSize);
}
hasMoreItems(): boolean {
return this.items.length > this.currentPageSize;
}
showMoreItems() {
this.currentPageSize += this.pageSize;
}
clear() {
this.currentPageSize = this.pageSize;
this.items = [];
}
}

View File

@@ -0,0 +1,86 @@
/*!
* @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 class SearchFilterList<T> implements Iterable<T> {
items: T[] = [];
pageSize: number = 5;
currentPageSize: number = 5;
get visibleItems(): T[] {
return this.items.slice(0, this.currentPageSize);
}
get length(): number {
return this.items.length;
}
get canShowMoreItems(): boolean {
return this.items.length > this.currentPageSize;
}
get canShowLessItems(): boolean {
return this.currentPageSize > this.pageSize;
}
get fitsPage(): boolean {
return this.pageSize > this.items.length;
}
constructor(items: T[] = [], pageSize: number = 5) {
this.items = items;
this.pageSize = pageSize;
this.currentPageSize = pageSize;
}
showMoreItems() {
if (this.canShowMoreItems) {
this.currentPageSize += this.pageSize;
}
}
showLessItems() {
if (this.canShowLessItems) {
this.currentPageSize -= this.pageSize;
}
}
clear() {
this.currentPageSize = this.pageSize;
this.items = [];
}
[Symbol.iterator](): Iterator<T> {
let pointer = 0;
let items = this.visibleItems;
return {
next(): IteratorResult<T> {
if (pointer < items.length) {
return {
done: false,
value: items[pointer++]
};
} else {
return {
done: true,
value: null
};
}
}
};
}
}

View File

@@ -22,20 +22,28 @@
<mat-panel-title>{{ facetQueriesLabel | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="checklist">
<ng-container *ngFor="let query of responseFacetQueries.visibleItems">
<ng-container *ngFor="let query of responseFacetQueries">
<mat-checkbox
[checked]="query.$checked"
(change)="onFacetQueryToggle($event, query)">
{{ query.label }} ({{ query.count }})
{{ query.label | translate }} ({{ query.count }})
</mat-checkbox>
</ng-container>
</div>
<button mat-button
*ngIf="responseFacetQueries.hasMoreItems()"
(click)="responseFacetQueries.showMoreItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
<div class="facet-buttons">
<button mat-button
*ngIf="responseFacetQueries.canShowLessItems"
(click)="responseFacetQueries.showLessItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-button
*ngIf="responseFacetQueries.canShowMoreItems"
(click)="responseFacetQueries.showMoreItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
<mat-expansion-panel
@@ -44,23 +52,31 @@
(opened)="onFacetFieldExpanded(field)"
(closed)="onFacetFieldCollapsed(field)">
<mat-expansion-panel-header>
<mat-panel-title>{{ field.label }}</mat-panel-title>
<mat-panel-title>{{ field.label | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="checklist">
<mat-checkbox
*ngFor="let bucket of field.getVisibleBuckets()"
*ngFor="let bucket of field.buckets"
[checked]="bucket.$checked"
(change)="onFacetToggle($event, field, bucket)">
{{ bucket.display || bucket.label }} ({{ bucket.count }})
{{ (bucket.display || bucket.label) | translate }} ({{ bucket.count }})
</mat-checkbox>
</div>
<button mat-button
*ngIf="field.hasMoreItems()"
(click)="field.showMoreItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
<div class="facet-buttons" *ngIf="!field.buckets.fitsPage">
<button mat-button
*ngIf="field.buckets.canShowLessItems"
(click)="field.buckets.showLessItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-button
*ngIf="field.buckets.canShowMoreItems"
(click)="field.buckets.showMoreItems()">
{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@@ -6,3 +6,11 @@
margin: 5px;
}
}
.facet-buttons {
text-align: right;
.mat-button {
text-transform: uppercase;
}
}

View File

@@ -307,8 +307,8 @@ describe('SearchSettingsComponent', () => {
component.onDataLoaded(data);
expect(component.responseFacetFields.length).toBe(2);
expect(component.responseFacetFields[0].buckets[0].$checked).toBeFalsy();
expect(component.responseFacetFields[1].buckets[0].$checked).toBeTruthy();
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', () => {

View File

@@ -24,6 +24,7 @@ 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 { SearchFilterList } from './models/search-filter-list.model';
@Component({
selector: 'adf-search-filter',
@@ -44,6 +45,8 @@ export class SearchFilterComponent implements OnInit {
facetQueriesExpanded = false;
constructor(public queryBuilder: SearchQueryBuilderService, private search: SearchService) {
this.responseFacetQueries = new ResponseFacetQueryList();
if (queryBuilder.config && queryBuilder.config.facetQueries) {
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || 5;
@@ -158,12 +161,12 @@ export class SearchFilterComponent implements OnInit {
const expandedFields = this.responseFacetFields.filter(f => f.expanded).map(f => f.label);
this.responseFacetFields = (context.facetsFields || []).map(
(field: ResponseFacetField) => {
field => {
field.pageSize = field.pageSize || 5;
field.currentPageSize = field.pageSize;
field.expanded = expandedFields.includes(field.label);
(field.buckets || []).forEach(bucket => {
const buckets = (field.buckets || []).map(bucket => {
bucket.$field = field.label;
bucket.$checked = false;
@@ -173,21 +176,9 @@ export class SearchFilterComponent implements OnInit {
if (previousBucket) {
bucket.$checked = true;
}
return bucket;
});
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);
};
field.buckets = new SearchFilterList<FacetFieldBucket>(buckets, field.pageSize);
return field;
}
);

View File

@@ -3,7 +3,8 @@
<mat-form-field>
<input
matInput [formControl]="from" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM' | translate }}">
placeholder="{{ 'SEARCH.FILTER.RANGE.FROM' | translate }}"
autocomplete="off">
<mat-error *ngIf="from.hasError('pattern')">
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
</mat-error>
@@ -12,17 +13,18 @@
</mat-error>
</mat-form-field>
<mat-form-field >
<mat-form-field>
<input
matInput [formControl]="to" [errorStateMatcher]="matcher"
placeholder="{{ 'SEARCH.FILTER.RANGE.TO' | translate }}">
placeholder="{{ 'SEARCH.FILTER.RANGE.TO' | translate }}"
autocomplete="off">
<mat-error *ngIf="to.invalid">
{{ 'SEARCH.FILTER.VALIDATION.INVALID-FORMAT' | translate }}
</mat-error>
</mat-form-field>
<div>
<div class="facet-buttons">
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>

View File

@@ -1,8 +1,5 @@
.adf-search-number-range > form {
display: inline-flex;
flex-direction: column;
.mat-button {
text-transform: uppercase;
}
width: 100%;
}

View File

@@ -1,6 +1,24 @@
<mat-radio-group [(ngModel)]="value" (change)="changeHandler($event)">
<mat-radio-group
[(ngModel)]="value"
(change)="changeHandler($event)">
<mat-radio-button
*ngFor="let option of settings.options" [value]="option.value">
{{ option.name }}
*ngFor="let option of options"
[value]="option.value">
{{ option.name | translate }}
</mat-radio-button>
</mat-radio-group>
<div class="facet-buttons" *ngIf="!options.fitsPage">
<button mat-icon-button
[disabled]="!options.canShowLessItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-LESS' | translate }}"
(click)="options.showLessItems()">
<mat-icon>keyboard_arrow_up</mat-icon>
</button>
<button mat-icon-button
[disabled]="!options.canShowMoreItems"
title="{{ 'SEARCH.FILTER.ACTIONS.SHOW-MORE' | translate }}"
(click)="options.showMoreItems()">
<mat-icon>keyboard_arrow_down</mat-icon>
</button>
</div>

View File

@@ -21,6 +21,12 @@ import { MatRadioChange } from '@angular/material';
import { SearchWidget } from '../../search-widget.interface';
import { SearchWidgetSettings } from '../../search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../search-query-builder.service';
import { SearchFilterList } from '../search-filter/models/search-filter-list.model';
export interface SearchRadioOption {
name: string;
value: string;
}
@Component({
selector: 'adf-search-radio',
@@ -37,8 +43,24 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
id: string;
settings: SearchWidgetSettings;
context: SearchQueryBuilderService;
options: SearchFilterList<SearchRadioOption>;
pageSize = 5;
constructor() {
this.options = new SearchFilterList<SearchRadioOption>();
}
ngOnInit() {
if (this.settings) {
this.pageSize = this.settings.pageSize || 5;
if (this.settings.options && this.settings.options.length > 0) {
this.options = new SearchFilterList<SearchRadioOption>(
this.settings.options, this.pageSize
);
}
}
this.setValue(
this.getSelectedValue()
);

View File

@@ -7,7 +7,7 @@
(change)="onChangedHandler($event)">
</mat-slider>
<div>
<div class="facet-buttons">
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
</button>

View File

@@ -16,15 +16,12 @@
*/
import { FacetFieldBucket } from './facet-field-bucket.interface';
import { SearchFilterList } from './components/search-filter/models/search-filter-list.model';
export interface ResponseFacetField {
label: string;
buckets: Array<FacetFieldBucket>;
buckets: SearchFilterList<FacetFieldBucket>;
pageSize?: number;
currentPageSize?: number;
expanded?: boolean;
hasMoreItems(): boolean;
showMoreItems(): void;
getVisibleBuckets(): Array<FacetFieldBucket>;
}