mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACA-4486] support search widget chips layout (#7122)
* [ACA-4486] support search widget chips layout * * revert to old config * * resolved rebase conflicts * [ci:force] force e2e * [ci:force] docs update and remove directive added * [ci:force] config updated * [ci:force] add missing app config schema to prod build
This commit is contained in:
@@ -50,7 +50,7 @@ import { CustomResourcesService } from '../document-list/services/custom-resourc
|
||||
import { NodeEntryEvent, ShareDataRow } from '../document-list';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../search/search-query-service.token';
|
||||
import { SearchQueryBuilderService } from '../search/search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../search/services/search-query-builder.service';
|
||||
import { ContentNodeSelectorPanelService } from './content-node-selector-panel.service';
|
||||
|
||||
export type ValidationFunction = (entry: Node) => boolean;
|
||||
|
@@ -29,7 +29,7 @@ import { CoreModule } from '@alfresco/adf-core';
|
||||
import { DocumentListModule } from '../document-list/document-list.module';
|
||||
import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component';
|
||||
import { UploadModule } from '../upload/upload.module';
|
||||
import { SearchQueryBuilderService } from '../search/search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../search/services/search-query-builder.service';
|
||||
import { ContentDirectiveModule } from '../directives/content-directive.module';
|
||||
|
||||
@NgModule({
|
||||
|
@@ -21,7 +21,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchService, setupTestBed, DataTableComponent, DataSorting } from '@alfresco/adf-core';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
import { SearchHeaderQueryBuilderService } from './../../../search/search-header-query-builder.service';
|
||||
import { SearchHeaderQueryBuilderService } from './../../../search/services/search-header-query-builder.service';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from './../../../search/search-query-service.token';
|
||||
import { DocumentListComponent } from './../document-list.component';
|
||||
import { FilterHeaderComponent } from './filter-header.component';
|
||||
|
@@ -19,7 +19,7 @@ import { Component, Inject, OnInit, OnChanges, SimpleChanges, Input, Output, Eve
|
||||
import { PaginationModel, DataSorting } from '@alfresco/adf-core';
|
||||
import { DocumentListComponent } from '../document-list.component';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search/search-query-service.token';
|
||||
import { SearchHeaderQueryBuilderService } from '../../../search/search-header-query-builder.service';
|
||||
import { SearchHeaderQueryBuilderService } from '../../../search/services/search-header-query-builder.service';
|
||||
import { FilterSearch } from './../../../search/models/filter-search.interface';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
@@ -223,14 +223,17 @@
|
||||
},
|
||||
"FILTER": {
|
||||
"ACTIONS": {
|
||||
"SEARCH": "Search",
|
||||
"CLEAR": "Clear",
|
||||
"APPLY": "Apply",
|
||||
"CLEAR-ALL": "Clear all",
|
||||
"SHOW-MORE": "Show more",
|
||||
"SHOW-LESS": "Show less",
|
||||
"FILTER-CATEGORY": "Filter category"
|
||||
"SHOW-LESS": "Show less"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CLOSE": "Close",
|
||||
"REMOVE": "Remove",
|
||||
"APPLY": "Apply",
|
||||
"CLEAR-ALL": {
|
||||
"LABEL": "Clear all",
|
||||
"TOOLTIP": "This will remove all selections"
|
||||
@@ -290,8 +293,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FORMS": "Search Forms",
|
||||
"UNKNOWN_FORM": "Unknown Configuration",
|
||||
"UNKNOWN_CONFIGURATION": "Unknown Configuration",
|
||||
"SEARCH_HEADER" : {
|
||||
"TITLE":"Filter",
|
||||
"TYPE": "Type",
|
||||
|
@@ -15,6 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SearchCategory } from '../search';
|
||||
|
||||
export const expandableCategories = [
|
||||
{
|
||||
id: 'cat-1',
|
||||
@@ -68,7 +70,7 @@ export const expandedCategories = [
|
||||
}
|
||||
];
|
||||
|
||||
export const simpleCategories = [
|
||||
export const simpleCategories: SearchCategory[] = [
|
||||
{
|
||||
id: 'queryName',
|
||||
name: 'Name',
|
||||
@@ -76,7 +78,9 @@ export const simpleCategories = [
|
||||
enabled: true,
|
||||
component: {
|
||||
selector: 'text',
|
||||
settings: {}
|
||||
settings: {
|
||||
field: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,7 +91,7 @@ export const simpleCategories = [
|
||||
component: {
|
||||
selector: 'check-list',
|
||||
settings: {
|
||||
'field': null,
|
||||
'field': 'check-list',
|
||||
'pageSize': 5,
|
||||
'options': [
|
||||
{ 'name': 'Folder', 'value': "TYPE:'cm:folder'" },
|
||||
@@ -624,6 +628,7 @@ export const mockContentSizeResponseBucket = {
|
||||
};
|
||||
|
||||
export function getMockSearchResultWithResponseBucket() {
|
||||
mockSearchResult.list.context.facets[3].buckets.push(mockContentSizeResponseBucket);
|
||||
return mockSearchResult;
|
||||
const cloneResult = JSON.parse(JSON.stringify( mockSearchResult));
|
||||
cloneResult.list.context.facets[3].buckets.push(mockContentSizeResponseBucket);
|
||||
return cloneResult;
|
||||
}
|
||||
|
@@ -0,0 +1,58 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||
import { SearchFacetFiltersService } from '../services/search-facet-filters.service';
|
||||
import { SearchQueryBuilderService } from '../services/search-query-builder.service';
|
||||
|
||||
@Component({
|
||||
template: `<button adf-reset-search></button>`
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
||||
|
||||
describe('Directive: ResetSearchDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
declarations: [TestComponent]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
});
|
||||
|
||||
it('should reset the search on click', () => {
|
||||
spyOn(queryBuilder, 'resetToDefaults');
|
||||
searchFacetFiltersService.responseFacets = [ { type: 'field', label: 'f1' } ] as any;
|
||||
fixture.nativeElement.querySelector('button').click();
|
||||
expect(searchFacetFiltersService.responseFacets).toEqual([]);
|
||||
expect(queryBuilder.resetToDefaults).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { Directive, HostListener } from '@angular/core';
|
||||
import { SearchFacetFiltersService } from '../services/search-facet-filters.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[adf-reset-search]'
|
||||
})
|
||||
export class ResetSearchDirective {
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
this.filterService.reset();
|
||||
}
|
||||
|
||||
constructor(private filterService: SearchFacetFiltersService) { }
|
||||
}
|
@@ -6,8 +6,7 @@
|
||||
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
||||
(change)="changeHandler($event, option)"
|
||||
class="adf-facet-filter">
|
||||
<div
|
||||
matTooltip="{{ option.name | translate }}"
|
||||
<div matTooltip="{{ option.name | translate }}"
|
||||
matTooltipPosition="right"
|
||||
class="facet-name">
|
||||
{{ option.name | translate }}
|
||||
@@ -15,9 +14,9 @@
|
||||
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="adf-facet-buttons" *ngIf="options.fitsPage">
|
||||
<button mat-button color="primary" (click)="reset()">
|
||||
|
||||
<div class="adf-facet-buttons" *ngIf="options.fitsPage && !settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" (click)="clear()">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -25,7 +24,7 @@
|
||||
<div class="adf-facet-buttons" *ngIf="!options.fitsPage">
|
||||
<button mat-icon-button
|
||||
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
|
||||
(click)="reset()">
|
||||
(click)="clear()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button
|
||||
|
@@ -15,12 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface SearchListOption {
|
||||
name: string;
|
||||
@@ -46,8 +48,9 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
||||
pageSize = 5;
|
||||
isActive = false;
|
||||
enableChangeUpdate = true;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
constructor() {
|
||||
constructor(private translationService: TranslationService) {
|
||||
this.options = new SearchFilterList<SearchListOption>();
|
||||
}
|
||||
|
||||
@@ -59,11 +62,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
||||
if (this.settings.options && this.settings.options.length > 0) {
|
||||
this.options = new SearchFilterList(this.settings.options, this.pageSize);
|
||||
}
|
||||
|
||||
if (this.settings.allowUpdateOnChange !== undefined &&
|
||||
this.settings.allowUpdateOnChange !== null) {
|
||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange;
|
||||
}
|
||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||
}
|
||||
|
||||
if (this.startValue) {
|
||||
@@ -71,18 +70,42 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
this.clearOptions();
|
||||
if (this.id && this.context && this.enableChangeUpdate) {
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
clearOptions() {
|
||||
this.options.items.forEach((opt) => {
|
||||
opt.checked = false;
|
||||
});
|
||||
|
||||
if (this.id && this.context) {
|
||||
this.context.queryFragments[this.id] = '';
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.isActive = false;
|
||||
this.clearOptions();
|
||||
if (this.id && this.context) {
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
const displayValue = this.options.items
|
||||
.filter((option) => option.checked)
|
||||
.map(({name}) => this.translationService.instant(name))
|
||||
.join(', ');
|
||||
this.displayValue$.next(displayValue);
|
||||
}
|
||||
|
||||
changeHandler(event: MatCheckboxChange, option: any) {
|
||||
option.checked = event.checked;
|
||||
const checkedValues = this.getCheckedValues();
|
||||
@@ -118,6 +141,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
||||
const query = checkedValues.join(` ${this.operator} `);
|
||||
if (this.id && this.context) {
|
||||
this.context.queryFragments[this.id] = query;
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,20 @@
|
||||
<mat-chip-list>
|
||||
<ng-container *ngIf="searchFilter && searchFilter.selectedBuckets.length">
|
||||
<mat-chip *ngIf="clearAll && searchFilter.selectedBuckets.length > 1"
|
||||
<ng-container *ngIf="facetFiltersService.selectedBuckets.length">
|
||||
<mat-chip *ngIf="clearAll && facetFiltersService.selectedBuckets.length > 1"
|
||||
data-automation-id="reset-filter"
|
||||
color="primary"
|
||||
selected
|
||||
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.TOOLTIP' | translate }}"
|
||||
matTooltipPosition="right"
|
||||
(click)="searchFilter.resetAllSelectedBuckets()">
|
||||
(click)="facetFiltersService.resetAllSelectedBuckets()">
|
||||
{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.LABEL' | translate }}
|
||||
</mat-chip>
|
||||
|
||||
<mat-chip
|
||||
data-automation-id="chip-list-entry"
|
||||
*ngFor="let selection of searchFilter.selectedBuckets"
|
||||
*ngFor="let selection of facetFiltersService.selectedBuckets"
|
||||
[removable]="true"
|
||||
(removed)="searchFilter.unselectFacetBucket(selection.field, selection.bucket)">
|
||||
(removed)="facetFiltersService.unselectFacetBucket(selection.field, selection.bucket)">
|
||||
{{ (selection.bucket.display || selection.bucket.label) | translate }}
|
||||
<mat-icon matChipRemove>cancel</mat-icon>
|
||||
</mat-chip>
|
||||
|
@@ -16,10 +16,9 @@
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SelectedBucket } from '../search-filter/search-filter.component';
|
||||
import { SearchFacetFiltersService, SelectedBucket } from '../../services/search-facet-filters.service';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@@ -43,20 +42,21 @@ class TestComponent {
|
||||
describe('SearchChipListComponent', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let component: TestComponent;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
declarations: [
|
||||
TestComponent
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
});
|
||||
|
||||
it('should display clear button only when entries present', () => {
|
||||
@@ -65,7 +65,7 @@ describe('SearchChipListComponent', () => {
|
||||
let clearButton = fixture.debugElement.query(By.css(`[data-automation-id="reset-filter"]`));
|
||||
expect(clearButton).toBeNull();
|
||||
|
||||
component.searchFilter.selectedBuckets = [{
|
||||
searchFacetFiltersService.selectedBuckets = [{
|
||||
bucket: {
|
||||
count: 1,
|
||||
label: 'test',
|
||||
@@ -79,7 +79,7 @@ describe('SearchChipListComponent', () => {
|
||||
});
|
||||
|
||||
it('should reflect changes in the search filter', () => {
|
||||
const selectedBuckets = component.searchFilter.selectedBuckets;
|
||||
const selectedBuckets = searchFacetFiltersService.selectedBuckets;
|
||||
fixture.detectChanges();
|
||||
|
||||
let chips = fixture.debugElement.queryAll(By.css(`[data-automation-id="chip-list-entry"]`));
|
||||
@@ -100,9 +100,9 @@ describe('SearchChipListComponent', () => {
|
||||
});
|
||||
|
||||
it('should remove the entry upon remove button click', async () => {
|
||||
spyOn(component.searchFilter, 'unselectFacetBucket').and.callThrough();
|
||||
spyOn(searchFacetFiltersService, 'unselectFacetBucket').and.callThrough();
|
||||
|
||||
component.searchFilter.selectedBuckets = [
|
||||
searchFacetFiltersService.selectedBuckets = [
|
||||
{
|
||||
bucket: {
|
||||
count: 1,
|
||||
@@ -119,15 +119,15 @@ describe('SearchChipListComponent', () => {
|
||||
chips[0].nativeElement.click();
|
||||
|
||||
await fixture.whenStable();
|
||||
expect(component.searchFilter.unselectFacetBucket).toHaveBeenCalled();
|
||||
expect(searchFacetFiltersService.unselectFacetBucket).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove items from the search filter on clear button click', () => {
|
||||
spyOn(component.searchFilter, 'unselectFacetBucket').and.stub();
|
||||
spyOn(searchFacetFiltersService, 'unselectFacetBucket').and.stub();
|
||||
|
||||
const selectedBucket1: any = { field: { id: 1 }, bucket: {label: 'bucket1'} };
|
||||
const selectedBucket2: any = { field: { id: 2 }, bucket: {label: 'bucket2'} };
|
||||
component.searchFilter.selectedBuckets = [selectedBucket1, selectedBucket2];
|
||||
searchFacetFiltersService.selectedBuckets = [selectedBucket1, selectedBucket2];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -137,14 +137,14 @@ describe('SearchChipListComponent', () => {
|
||||
closeButtons[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.searchFilter.unselectFacetBucket).toHaveBeenCalledWith(selectedBucket1.field, selectedBucket1.bucket);
|
||||
expect(searchFacetFiltersService.unselectFacetBucket).toHaveBeenCalledWith(selectedBucket1.field, selectedBucket1.bucket);
|
||||
});
|
||||
|
||||
it('should disable clear mode via input properties', () => {
|
||||
spyOn(component.searchFilter, 'unselectFacetBucket').and.callThrough();
|
||||
|
||||
component.allowClear = false;
|
||||
component.searchFilter.selectedBuckets = [
|
||||
searchFacetFiltersService.selectedBuckets = [
|
||||
{
|
||||
bucket: {
|
||||
count: 1,
|
||||
|
@@ -17,6 +17,7 @@
|
||||
|
||||
import { Component, ViewEncapsulation, Input } from '@angular/core';
|
||||
import { SearchFilterComponent } from '../../components/search-filter/search-filter.component';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-chip-list',
|
||||
@@ -26,11 +27,13 @@ import { SearchFilterComponent } from '../../components/search-filter/search-fil
|
||||
})
|
||||
export class SearchChipListComponent {
|
||||
|
||||
/** Search filter to supply the data for the chips. */
|
||||
@Input()
|
||||
/** @deprecated This is not required since ADF 4.5.0 */
|
||||
searchFilter: SearchFilterComponent;
|
||||
|
||||
/** Flag used to enable the display of a clear-all-filters button. */
|
||||
@Input()
|
||||
clearAll: boolean = false;
|
||||
|
||||
constructor(public facetFiltersService: SearchFacetFiltersService) {}
|
||||
}
|
||||
|
@@ -34,8 +34,8 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace">
|
||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="date-range-clear-btn">
|
||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace" *ngIf="!settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="date-range-clear-btn">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="date-range-apply-btn">
|
||||
|
@@ -15,14 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OnInit, Component, ViewEncapsulation, OnDestroy } from '@angular/core';
|
||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MomentDateAdapter, MOMENT_DATE_FORMATS, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
|
||||
import {
|
||||
MOMENT_DATE_FORMATS,
|
||||
MomentDateAdapter,
|
||||
UserPreferencesService,
|
||||
UserPreferenceValues
|
||||
} from '@alfresco/adf-core';
|
||||
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||
import { Moment } from 'moment';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -64,6 +69,8 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
fromMaxDate: any;
|
||||
isActive = false;
|
||||
startValue: any;
|
||||
enableChangeUpdate: boolean;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
@@ -126,6 +133,7 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
});
|
||||
|
||||
this.setFromMaxDate();
|
||||
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -141,6 +149,8 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
const end = moment(model.to).endOf('day').format();
|
||||
|
||||
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
||||
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
@@ -160,6 +170,14 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
};
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
if (this.form.invalid || this.form.pristine) {
|
||||
this.displayValue$.next('');
|
||||
} else {
|
||||
this.displayValue$.next(`${this.dateAdapter.format(this.form.value.from, this.datePickerFormat)} - ${this.dateAdapter.format(this.form.value.to, this.datePickerFormat)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(parsedDate: string) {
|
||||
const splitValue = parsedDate.split('||');
|
||||
const fromValue = this.dateAdapter.parse(splitValue[0], this.datePickerFormat);
|
||||
@@ -172,20 +190,34 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
this.to.markAsTouched();
|
||||
this.submitValues();
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
this.form.reset({
|
||||
from: '',
|
||||
to: ''
|
||||
});
|
||||
|
||||
if (this.id && this.context) {
|
||||
this.context.queryFragments[this.id] = '';
|
||||
this.context.update();
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateQuery();
|
||||
}
|
||||
}
|
||||
this.setFromMaxDate();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clear();
|
||||
this.updateQuery();
|
||||
}
|
||||
|
||||
private updateQuery() {
|
||||
if (this.id && this.context) {
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
onChangedHandler(event: any, formControl: FormControl) {
|
||||
|
||||
const inputValue = event.value;
|
||||
|
@@ -34,8 +34,8 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace">
|
||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="datetime-range-clear-btn">
|
||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace" *ngIf="!settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="datetime-range-clear-btn">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="datetime-range-apply-btn">
|
||||
|
@@ -15,13 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OnInit, Component, ViewEncapsulation, OnDestroy } from '@angular/core';
|
||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
|
||||
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||
import { Moment } from 'moment';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -64,6 +64,8 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
fromMaxDatetime: any;
|
||||
isActive = false;
|
||||
startValue: any;
|
||||
enableChangeUpdate: boolean;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
@@ -119,6 +121,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
});
|
||||
|
||||
this.setFromMaxDatetime();
|
||||
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -134,6 +137,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
const end = moment.utc(model.to).endOf('minute').format();
|
||||
|
||||
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
@@ -149,10 +153,18 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
getCurrentValue(): DatetimeRangeValue {
|
||||
return {
|
||||
from: this.dateAdapter.format(this.form.value.from, this.datetimePickerFormat),
|
||||
to: this.dateAdapter.format(this.form.value.from, this.datetimePickerFormat)
|
||||
to: this.dateAdapter.format(this.form.value.to, this.datetimePickerFormat)
|
||||
};
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
if (this.form.invalid || this.form.pristine) {
|
||||
this.displayValue$.next('');
|
||||
} else {
|
||||
this.displayValue$.next(`${this.dateAdapter.format(this.form.value.from, this.datetimePickerFormat)} - ${this.dateAdapter.format(this.form.value.to, this.datetimePickerFormat)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(parsedDate: string) {
|
||||
const splitValue = parsedDate.split('||');
|
||||
const fromValue = this.dateAdapter.parse(splitValue[0], this.datetimePickerFormat);
|
||||
@@ -166,7 +178,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
this.submitValues();
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
this.form.reset({
|
||||
from: '',
|
||||
@@ -174,11 +186,26 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
||||
});
|
||||
if (this.id && this.context) {
|
||||
this.context.queryFragments[this.id] = '';
|
||||
this.context.update();
|
||||
}
|
||||
|
||||
if (this.id && this.context && this.enableChangeUpdate) {
|
||||
this.updateQuery();
|
||||
}
|
||||
this.setFromMaxDatetime();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clear();
|
||||
this.updateQuery();
|
||||
}
|
||||
|
||||
private updateQuery() {
|
||||
if (this.id && this.context) {
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
onChangedHandler(event: any, formControl: FormControl) {
|
||||
|
||||
const inputValue = event.value;
|
||||
|
@@ -0,0 +1,49 @@
|
||||
<div class="adf-search-filter-facet">
|
||||
<div class="adf-facet-result-filter">
|
||||
<div class="adf-facet-search-container">
|
||||
<button mat-icon-button class="adf-facet-search-icon" tabindex="-1">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
<mat-form-field class="adf-facet-search-field" floatLabel="never">
|
||||
<input matInput placeholder="{{ 'SEARCH.FILTER.ACTIONS.SEARCH' | translate }}"
|
||||
[attr.data-automation-id]="'facet-result-filter-'+field.label" [(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>
|
||||
|
||||
<div class="adf-checklist">
|
||||
<mat-checkbox *ngFor="let bucket of field.buckets" [checked]="bucket.checked"
|
||||
[attr.data-automation-id]="'checkbox-'+field.label+'-'+(bucket.display || bucket.label)"
|
||||
(change)="onToggleBucket($event, field, bucket)">
|
||||
<div matTooltip="{{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }}"
|
||||
matTooltipPosition="right" class="adf-facet-label">
|
||||
{{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }}
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="adf-facet-buttons" *ngIf="field.buckets.fitsPage && !field.settings?.hideDefaultAction">
|
||||
<button *ngIf="canResetSelectedBuckets(field)" mat-button color="primary" (click)="resetSelectedBuckets(field)">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="adf-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>
|
||||
</div>
|
@@ -0,0 +1,89 @@
|
||||
@mixin adf-search-filter-field-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.adf-search-filter-facet {
|
||||
.adf-checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mat-checkbox-label {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-facet-label {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-checkbox {
|
||||
margin: 5px;
|
||||
|
||||
&.mat-checkbox-checked .mat-checkbox-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-facet-result-filter {
|
||||
padding-bottom: 16px;
|
||||
|
||||
.adf-facet-search-container {
|
||||
border-radius: 6px;
|
||||
background: mat-color($background, background);
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
.adf-facet-search-icon {
|
||||
width: 27px;
|
||||
margin-top: -4px;
|
||||
.mat-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-facet-search-field {
|
||||
padding: 2px;
|
||||
flex: 1;
|
||||
margin-top: -16px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.25px;
|
||||
|
||||
.mat-form-field-underline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mat-form-field-suffix {
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-facet-buttons {
|
||||
text-align: right;
|
||||
|
||||
.mat-button {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--topSpace {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-checkbox-label,
|
||||
.mat-radio-label {
|
||||
color: mat-color($foreground, text, 0.54);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchFacetFieldComponent } from './search-facet-field.component';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { FacetField } from '../../models/facet-field.interface';
|
||||
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('SearchFacetFieldComponent', () => {
|
||||
let component: SearchFacetFieldComponent;
|
||||
let fixture: ComponentFixture<SearchFacetFieldComponent>;
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFacetFieldComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(searchFacetFiltersService, 'updateSelectedBuckets').and.stub();
|
||||
});
|
||||
|
||||
it('should update bucket model and query builder on facet toggle', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: true };
|
||||
const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() };
|
||||
const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 };
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.onToggleBucket(event, field, bucket);
|
||||
|
||||
expect(bucket.checked).toBeTruthy();
|
||||
expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(field, bucket);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update bucket model and query builder on facet un-toggle', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: false };
|
||||
const field: FacetField = { field: 'f1', label: 'f1', buckets: new SearchFilterList() };
|
||||
const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 };
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.onToggleBucket(event, field, bucket);
|
||||
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, bucket);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unselect facet query and update builder', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: false };
|
||||
const query = { checked: true, label: 'q1', filterQuery: 'query1' };
|
||||
const field = { field: 'q1', type: 'query', label: 'label1', buckets: new SearchFilterList([ query ] ) } as FacetField;
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.onToggleBucket(event, <any> field, <any> query);
|
||||
|
||||
expect(query.checked).toEqual(false);
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, query);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(searchFacetFiltersService.updateSelectedBuckets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update query builder only when has bucket to unselect', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
const field: FacetField = { field: 'f1', label: 'f1' };
|
||||
component.onToggleBucket(<any> { checked: true }, field, null);
|
||||
|
||||
expect(queryBuilder.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow to to reset selected buckets', () => {
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canResetSelectedBuckets(field)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not allow to reset selected buckets', () => {
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.canResetSelectedBuckets(field)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should reset selected buckets', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.resetSelectedBuckets(field);
|
||||
|
||||
expect(buckets[0].checked).toEqual(false);
|
||||
expect(buckets[1].checked).toEqual(false);
|
||||
});
|
||||
|
||||
it('should update query builder upon resetting buckets', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.field = field;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.resetSelectedBuckets(field);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,129 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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, Inject, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { FacetField } from '../../models/facet-field.interface';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
import { FacetWidget } from '../../models/facet-widget.interface';
|
||||
import { TranslationService } from '@alfresco/adf-core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-facet-field',
|
||||
templateUrl: './search-facet-field.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchFacetFieldComponent implements FacetWidget {
|
||||
|
||||
@Input()
|
||||
field!: FacetField;
|
||||
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
||||
private searchFacetFiltersService: SearchFacetFiltersService,
|
||||
private translationService: TranslationService) {
|
||||
}
|
||||
|
||||
get canUpdateOnChange() {
|
||||
return this.field.settings?.allowUpdateOnChange ?? true;
|
||||
}
|
||||
|
||||
onToggleBucket(event: MatCheckboxChange, field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (event && bucket) {
|
||||
if (event.checked) {
|
||||
this.selectFacetBucket(field, bucket);
|
||||
} else {
|
||||
this.unselectFacetBucket(field, bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (bucket) {
|
||||
bucket.checked = true;
|
||||
this.queryBuilder.addUserFacetBucket(field, bucket);
|
||||
this.searchFacetFiltersService.updateSelectedBuckets();
|
||||
if (this.canUpdateOnChange) {
|
||||
this.updateDisplayValue();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (bucket) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
this.searchFacetFiltersService.updateSelectedBuckets();
|
||||
if (this.canUpdateOnChange) {
|
||||
this.updateDisplayValue();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canResetSelectedBuckets(field: FacetField): boolean {
|
||||
if (field && field.buckets) {
|
||||
return field.buckets.items.some((bucket) => bucket.checked);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resetSelectedBuckets(field: FacetField) {
|
||||
if (field && field.buckets) {
|
||||
for (const bucket of field.buckets.items) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
}
|
||||
this.searchFacetFiltersService.updateSelectedBuckets();
|
||||
if (this.canUpdateOnChange) {
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBucketCountDisplay(bucket: FacetFieldBucket): string {
|
||||
return bucket.count === null ? '' : `(${bucket.count})`;
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
if (!this.field.buckets?.items) {
|
||||
this.displayValue$.next('');
|
||||
} else {
|
||||
const displayValue = this.field.buckets?.items?.filter((item) => item.checked)
|
||||
.map((item) => this.translationService.instant(item.display || item.label))
|
||||
.join(', ');
|
||||
this.displayValue$.next(displayValue);
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.resetSelectedBuckets(this.field);
|
||||
this.updateDisplayValue();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
|
||||
submitValues(): void {
|
||||
this.updateDisplayValue();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
<mat-chip [attr.data-automation-id]="'search-filter-chip-' + field.label"
|
||||
disableRipple
|
||||
class="adf-search-filter-chip"
|
||||
[class.adf-search-toggle-chip]="(facetField.displayValue$ | async) || menuTrigger.menuOpen"
|
||||
[matMenuTriggerFor]="menu"
|
||||
(onMenuOpen)="onMenuOpen()"
|
||||
[attr.title]="facetField.displayValue$ | async"
|
||||
#menuTrigger="matMenuTrigger">
|
||||
|
||||
<span class="adf-search-filter-placeholder">
|
||||
<span class="adf-search-filter-ellipsis">{{ field.label | translate }}</span>
|
||||
<ng-container *ngIf="facetField.displayValue$ | async">:</ng-container>
|
||||
</span>
|
||||
|
||||
<span class="adf-search-filter-ellipsis" *ngIf="facetField.displayValue$ | async as displayValue">
|
||||
{{ displayValue | translate }}
|
||||
</span>
|
||||
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||
</mat-chip>
|
||||
|
||||
<mat-menu #menu="matMenu" backdropClass="adf-search-filter-chip-menu" (closed)="onClosed()">
|
||||
<div #menuContainer [attr.data-automation-id]="'search-field-' + field.label">
|
||||
<adf-search-filter-menu-card (click)="$event.stopPropagation()"
|
||||
(keydown.tab)="$event.stopPropagation();"
|
||||
(close)="menuTrigger.closeMenu()">
|
||||
<ng-container ngProjectAs="filter-title">
|
||||
{{ field.label | translate }}
|
||||
</ng-container>
|
||||
<ng-container ngProjectAs="filter-content">
|
||||
<adf-search-facet-field [field]="field" #facetField></adf-search-facet-field>
|
||||
</ng-container>
|
||||
<ng-container ngProjectAs="filter-actions">
|
||||
<button mat-flat-button class="adf-search-action-button" color="accent" (click)="onRemove()" id="cancel-filter-button">
|
||||
{{ 'SEARCH.FILTER.BUTTONS.REMOVE' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button class="adf-search-action-button" color="primary" (click)="onApply()" id="apply-filter-button">
|
||||
{{ 'SEARCH.FILTER.BUTTONS.APPLY' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</adf-search-filter-menu-card>
|
||||
</div>
|
||||
</mat-menu>
|
@@ -0,0 +1,66 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchFacetChipComponent } from './search-facet-chip.component';
|
||||
import { ContentTestingModule } from '../../../../testing/content.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchQueryBuilderService } from '../../../services/search-query-builder.service';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
import { SearchFilterList } from '../../../models/search-filter-list.model';
|
||||
|
||||
describe('SearchFacetChipComponent', () => {
|
||||
let component: SearchFacetChipComponent;
|
||||
let fixture: ComponentFixture<SearchFacetChipComponent>;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFacetChipComponent);
|
||||
component = fixture.componentInstance;
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
component.field = { type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() };
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should update search query on apply click', () => {
|
||||
const chip = fixture.debugElement.query(By.css('mat-chip'));
|
||||
chip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
|
||||
applyButton.triggerEventHandler('click', {});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update search query on cancel click', () => {
|
||||
const chip = fixture.debugElement.query(By.css('mat-chip'));
|
||||
chip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button'));
|
||||
applyButton.triggerEventHandler('click', {});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,67 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
|
||||
import { FacetField } from '../../../models/facet-field.interface';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { SearchFacetFieldComponent } from '../../search-facet-field/search-facet-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-facet-chip',
|
||||
templateUrl: './search-facet-chip.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchFacetChipComponent {
|
||||
@Input()
|
||||
field: FacetField;
|
||||
|
||||
@ViewChild('menuContainer', { static: false })
|
||||
menuContainer: ElementRef;
|
||||
|
||||
@ViewChild('menuTrigger', { static: false })
|
||||
menuTrigger: MatMenuTrigger;
|
||||
|
||||
@ViewChild(SearchFacetFieldComponent, { static: false })
|
||||
facetFieldComponent: SearchFacetFieldComponent;
|
||||
|
||||
focusTrap: ConfigurableFocusTrap;
|
||||
|
||||
constructor(private focusTrapFactory: ConfigurableFocusTrapFactory) {}
|
||||
|
||||
onMenuOpen() {
|
||||
if (this.menuContainer && !this.focusTrap) {
|
||||
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
|
||||
this.focusTrap.focusInitialElement();
|
||||
}
|
||||
}
|
||||
|
||||
onClosed() {
|
||||
this.focusTrap.destroy();
|
||||
this.focusTrap = null;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this.facetFieldComponent.reset();
|
||||
this.menuTrigger.closeMenu();
|
||||
}
|
||||
|
||||
onApply() {
|
||||
this.facetFieldComponent.submitValues();
|
||||
this.menuTrigger.closeMenu();
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<mat-chip-list aria-orientation="horizontal">
|
||||
<ng-container *ngFor="let category of queryBuilder.categories">
|
||||
<adf-search-widget-chip [category]="category"></adf-search-widget-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="facetFiltersService.responseFacets && showContextFacets">
|
||||
<ng-container *ngFor="let field of facetFiltersService.responseFacets">
|
||||
<adf-search-facet-chip [field]="field" [attr.data-automation-id]="'search-fact-chip-' + field.field" ></adf-search-facet-chip>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</mat-chip-list>
|
@@ -0,0 +1,66 @@
|
||||
@mixin adf-search-filter-chips-theme($theme) {
|
||||
$accent: map-get($theme, accent);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$unselected-background: mat-color($background, unselected-chip);
|
||||
$unselected-foreground: mat-color($foreground, text);
|
||||
$selected-chip-background: mat-color($background, card);
|
||||
$chip-placeholder: mat-color($foreground, disabled-text);
|
||||
|
||||
.adf-search-filter-chip {
|
||||
|
||||
&.mat-chip {
|
||||
border: 2px solid transparent;
|
||||
transition : border 500ms ease-in-out;
|
||||
max-width: 320px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
background: $unselected-background;
|
||||
|
||||
&:focus {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
&.mat-standard-chip::after {
|
||||
background: $unselected-background;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
&.mat-chip-list-wrapper {
|
||||
margin: 4px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.adf-search-toggle-chip {
|
||||
background: $selected-chip-background;
|
||||
border: 2px solid mat-color($accent);
|
||||
|
||||
&.mat-chip::after {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-search-filter-placeholder {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
color: $chip-placeholder;
|
||||
}
|
||||
|
||||
.adf-search-filter-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
padding-top: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&-menu + * .cdk-overlay-pane .mat-menu-panel {
|
||||
min-width: 320px;
|
||||
border-radius: 12px;
|
||||
@include mat-elevation(2);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,413 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchFilterChipsComponent } from './search-filter-chips.component';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFacetFieldComponent } from '../search-facet-field/search-facet-field.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import {
|
||||
disabledCategories,
|
||||
filteredResult,
|
||||
mockSearchResult,
|
||||
searchFilter,
|
||||
simpleCategories,
|
||||
stepOne,
|
||||
stepThree,
|
||||
stepTwo
|
||||
} from '../../../mock';
|
||||
import { getAllMenus } from '../search-filter/search-filter.component.spec';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
|
||||
describe('SearchFilterChipsComponent', () => {
|
||||
let fixture: ComponentFixture<SearchFilterChipsComponent>;
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
let appConfigService: AppConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
fixture = TestBed.createComponent(SearchFilterChipsComponent);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the already checked items', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.responseFacets = [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()}
|
||||
];
|
||||
searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
||||
{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' },
|
||||
{ label: 'b2', metrics: [{value: {count: 1}}], filterQuery: 'filter2' }] },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip'));
|
||||
facetChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item');
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the newly checked items', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList()}
|
||||
];
|
||||
queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
||||
{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' },
|
||||
{ label: 'b2', metrics: [{value: {count: 1}}], filterQuery: 'filter2' }] },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip'));
|
||||
facetChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item');
|
||||
});
|
||||
|
||||
it('should show buckets with 0 values when there are no facet fields on the response payload', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList( [
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() }
|
||||
];
|
||||
queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
const data = {
|
||||
list: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetChip = fixture.debugElement.query(By.css('[data-automation-id="search-fact-chip-f1"] mat-chip'));
|
||||
facetChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(0);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update query builder upon resetting selected queries', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
|
||||
|
||||
const queryResponse = {
|
||||
field: 'query-response',
|
||||
label: 'query response',
|
||||
buckets: new SearchFilterList([
|
||||
{ label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }])
|
||||
} as any;
|
||||
searchFacetFiltersService.responseFacets = [queryResponse];
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetChip = fixture.debugElement.query(By.css(`[data-automation-id="search-fact-chip-query-response"] mat-chip`));
|
||||
facetChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
|
||||
facetField.resetSelectedBuckets(queryResponse);
|
||||
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
|
||||
for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) {
|
||||
expect(entry.checked).toEqual(false);
|
||||
}
|
||||
});
|
||||
|
||||
describe('widgets', () => {
|
||||
|
||||
it('should not show the disabled widget', async () => {
|
||||
appConfigService.config.search = { categories: disabledCategories };
|
||||
queryBuilder.resetToDefaults();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
|
||||
expect(chips.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should show the widgets only if configured', async () => {
|
||||
appConfigService.config.search = { categories: simpleCategories };
|
||||
queryBuilder.resetToDefaults();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
|
||||
expect(chips.length).toBe(2);
|
||||
|
||||
const titleElements = fixture.debugElement.queryAll(By.css('.adf-search-filter-placeholder'));
|
||||
expect(titleElements.map(title => title.nativeElement.innerText.trim())).toEqual(['Name', 'Type']);
|
||||
});
|
||||
|
||||
it('should be update the search query when name changed', async () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
let chips = fixture.debugElement.queryAll(By.css('mat-chip'));
|
||||
expect(chips.length).toBe(6);
|
||||
|
||||
fixture.detectChanges();
|
||||
const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Name"]`));
|
||||
searchChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
|
||||
const inputElement = fixture.debugElement.query(By.css('[data-automation-id="search-field-Name"] input'));
|
||||
inputElement.triggerEventHandler('change', { target: { value: '*' } });
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
chips = fixture.debugElement.queryAll(By.css('mat-chip'));
|
||||
expect(chips.length).toBe(8);
|
||||
});
|
||||
|
||||
it('should show the long facet options list with pagination', () => {
|
||||
const field = `[data-automation-id="search-field-Size facet queries"]`;
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.detectChanges();
|
||||
const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Size facet queries"]`));
|
||||
searchChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
|
||||
let sizes = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(sizes).toEqual(stepOne);
|
||||
|
||||
let moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`));
|
||||
let lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`));
|
||||
|
||||
expect(lessButton).toEqual(null);
|
||||
expect(moreButton).toBeDefined();
|
||||
|
||||
moreButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
|
||||
sizes = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(sizes).toEqual(stepTwo);
|
||||
|
||||
moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`));
|
||||
lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`));
|
||||
expect(lessButton).toBeDefined();
|
||||
expect(moreButton).toBeDefined();
|
||||
|
||||
moreButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
sizes = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
|
||||
expect(sizes).toEqual(stepThree);
|
||||
|
||||
moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`));
|
||||
lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`));
|
||||
expect(lessButton).toBeDefined();
|
||||
expect(moreButton).toEqual(null);
|
||||
|
||||
lessButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
|
||||
sizes = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(sizes).toEqual(stepTwo);
|
||||
|
||||
moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`));
|
||||
lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`));
|
||||
expect(lessButton).toBeDefined();
|
||||
expect(moreButton).toBeDefined();
|
||||
|
||||
lessButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
|
||||
sizes = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(sizes).toEqual(stepOne);
|
||||
|
||||
moreButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-MORE"]`));
|
||||
lessButton = fixture.debugElement.query(By.css(`${field} button[title="SEARCH.FILTER.ACTIONS.SHOW-LESS"]`));
|
||||
expect(lessButton).toEqual(null);
|
||||
expect(moreButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show facets if filter is not available', () => {
|
||||
const chip = '[data-automation-id="search-filter-chip-Size facet queries"]';
|
||||
const filter = { ...searchFilter };
|
||||
delete filter.facetQueries;
|
||||
|
||||
appConfigService.config.search = filter;
|
||||
queryBuilder.resetToDefaults();
|
||||
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
|
||||
const facetElement = fixture.debugElement.query(By.css(chip));
|
||||
expect(facetElement).toEqual(null);
|
||||
});
|
||||
|
||||
it('should search the facets options and select it', () => {
|
||||
const field = `[data-automation-id="search-field-Size facet queries"]`;
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
const searchChip = fixture.debugElement.query(By.css(`[data-automation-id="search-filter-chip-Size facet queries"]`));
|
||||
searchChip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
|
||||
const inputElement = fixture.debugElement.query(By.css(`${field} input`));
|
||||
inputElement.nativeElement.value = 'Extra';
|
||||
inputElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
|
||||
let filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(filteredMenu).toEqual(['Extra Small (10239)']);
|
||||
|
||||
inputElement.nativeElement.value = 'my';
|
||||
inputElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
|
||||
filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(filteredMenu).toEqual(filteredResult);
|
||||
|
||||
const clearButton = fixture.debugElement.query(By.css(`${field} mat-form-field button`));
|
||||
clearButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
|
||||
filteredMenu = getAllMenus(`${field} mat-checkbox`, fixture);
|
||||
expect(filteredMenu).toEqual(stepOne);
|
||||
|
||||
const firstOption = fixture.debugElement.query(By.css(`${field} mat-checkbox`));
|
||||
firstOption.triggerEventHandler('change', { checked: true });
|
||||
fixture.detectChanges();
|
||||
|
||||
const checkedOption = fixture.debugElement.query(By.css(`${field} mat-checkbox.mat-checkbox-checked`));
|
||||
expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)');
|
||||
|
||||
expect(queryBuilder.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -0,0 +1,38 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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, Inject, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-filter-chips',
|
||||
templateUrl: './search-filter-chips.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchFilterChipsComponent {
|
||||
/** Toggles whether to show or not the context facet filters. */
|
||||
@Input()
|
||||
showContextFacets: boolean = true;
|
||||
|
||||
constructor(
|
||||
@Inject(SEARCH_QUERY_SERVICE_TOKEN)
|
||||
public queryBuilder: SearchQueryBuilderService,
|
||||
public facetFiltersService: SearchFacetFiltersService) {}
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<div class="adf-search-filter-menu-card">
|
||||
<div class="adf-search-filter-title">
|
||||
<ng-content select="filter-title"></ng-content>
|
||||
<mat-icon class="adf-search-filter-title-action"
|
||||
(click)="onClose()"
|
||||
[matTooltip]="'SEARCH.FILTER.BUTTONS.CLOSE' | translate">
|
||||
close
|
||||
</mat-icon>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="adf-search-filter-content">
|
||||
<ng-content select="filter-content"></ng-content>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="adf-search-filter-actions">
|
||||
<ng-content select="filter-actions"></ng-content>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,39 @@
|
||||
@mixin adf-search-filter-menu-card($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.adf-search-filter-menu-card {
|
||||
color: mat-color($foreground, text);
|
||||
background: mat-color($background, card);
|
||||
|
||||
.adf-search-filter-title {
|
||||
padding: 16px 12px;
|
||||
height: 32px;
|
||||
flex: 1 1 auto;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.15px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
font-style: inherit;
|
||||
|
||||
&-action {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-search-filter-content {
|
||||
padding: 16px 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.adf-search-filter-actions {
|
||||
padding: 16px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.adf-search-action-button {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchFilterMenuCardComponent } from './search-filter-menu-card.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentTestingModule } from '../../../../testing/content.testing.module';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
|
||||
describe('SearchFilterMenuComponent', () => {
|
||||
let component: SearchFilterMenuCardComponent;
|
||||
let fixture: ComponentFixture<SearchFilterMenuCardComponent>;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFilterMenuCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit on close click', (done) => {
|
||||
component.close.subscribe(() => done());
|
||||
const closButton = fixture.debugElement.nativeElement.querySelector('.adf-search-filter-title-action');
|
||||
closButton.click();
|
||||
});
|
||||
});
|
@@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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, EventEmitter, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-filter-menu-card',
|
||||
templateUrl: './search-filter-menu-card.component.html'
|
||||
})
|
||||
export class SearchFilterMenuCardComponent {
|
||||
@Output()
|
||||
close = new EventEmitter();
|
||||
|
||||
onClose() {
|
||||
this.close.emit();
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<mat-chip [attr.data-automation-id]="'search-filter-chip-' + category.name"
|
||||
disableRipple
|
||||
class="adf-search-filter-chip"
|
||||
[class.adf-search-toggle-chip]="(widget.getDisplayValue() | async) || menuTrigger.menuOpen"
|
||||
[matMenuTriggerFor]="menu"
|
||||
(onMenuOpen)="onMenuOpen()"
|
||||
[attr.title]="widget.getDisplayValue() | async"
|
||||
#menuTrigger="matMenuTrigger">
|
||||
<span class="adf-search-filter-placeholder">
|
||||
<span class="adf-search-filter-ellipsis">{{ category.name | translate }}</span>
|
||||
<ng-container *ngIf="widget.getDisplayValue() | async">:</ng-container>
|
||||
</span>
|
||||
<span class="adf-search-filter-ellipsis" *ngIf="widget.getDisplayValue() | async as displayValue">
|
||||
{{ displayValue | translate }}
|
||||
</span>
|
||||
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||
</mat-chip>
|
||||
|
||||
<mat-menu #menu="matMenu" backdropClass="adf-search-filter-chip-menu" (closed)="onClosed()">
|
||||
<div #menuContainer [attr.data-automation-id]="'search-field-' + category.name">
|
||||
<adf-search-filter-menu-card (click)="$event.stopPropagation()"
|
||||
(keydown.tab)="$event.stopPropagation();"
|
||||
(close)="menuTrigger.closeMenu()">
|
||||
|
||||
<ng-container ngProjectAs="filter-title">
|
||||
{{ category.name | translate }} <ng-container *ngIf="category.component.settings.unit">({{category.component.settings.unit}})</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="filter-content">
|
||||
<adf-search-widget-container #widget
|
||||
[id]="category.id"
|
||||
[selector]="category.component.selector"
|
||||
[settings]="category.component.settings">
|
||||
</adf-search-widget-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="filter-actions">
|
||||
<button mat-flat-button class="adf-search-action-button" color="accent" (click)="onRemove()" id="cancel-filter-button">
|
||||
{{ 'SEARCH.FILTER.BUTTONS.REMOVE' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button class="adf-search-action-button" color="primary" (click)="onApply()" id="apply-filter-button">
|
||||
{{ 'SEARCH.FILTER.BUTTONS.APPLY' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</adf-search-filter-menu-card>
|
||||
</div>
|
||||
</mat-menu>
|
@@ -0,0 +1,68 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchWidgetChipComponent } from './search-widget-chip.component';
|
||||
import { simpleCategories } from '../../../../mock';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentTestingModule } from '../../../../testing/content.testing.module';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchQueryBuilderService } from '../../../services/search-query-builder.service';
|
||||
|
||||
describe('SearchWidgetChipComponent', () => {
|
||||
let component: SearchWidgetChipComponent;
|
||||
let fixture: ComponentFixture<SearchWidgetChipComponent>;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
setupTestBed( {
|
||||
imports: [
|
||||
MatMenuModule,
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
fixture = TestBed.createComponent(SearchWidgetChipComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
component.category = simpleCategories[1];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should update search query on apply click', () => {
|
||||
const chip = fixture.debugElement.query(By.css('mat-chip'));
|
||||
chip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
|
||||
applyButton.triggerEventHandler('click', {});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update search query on cancel click', () => {
|
||||
const chip = fixture.debugElement.query(By.css('mat-chip'));
|
||||
chip.triggerEventHandler('click', { stopPropagation: () => null });
|
||||
fixture.detectChanges();
|
||||
const applyButton = fixture.debugElement.query(By.css('#cancel-filter-button'));
|
||||
applyButton.triggerEventHandler('click', {});
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@@ -0,0 +1,68 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchCategory } from '../../../models/search-category.interface';
|
||||
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { SearchWidgetContainerComponent } from '../../search-widget-container/search-widget-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-widget-chip',
|
||||
templateUrl: './search-widget-chip.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchWidgetChipComponent {
|
||||
|
||||
@Input()
|
||||
category: SearchCategory;
|
||||
|
||||
@ViewChild('menuContainer', { static: false })
|
||||
menuContainer: ElementRef;
|
||||
|
||||
@ViewChild('menuTrigger', { static: false })
|
||||
menuTrigger: MatMenuTrigger;
|
||||
|
||||
@ViewChild(SearchWidgetContainerComponent, { static: false })
|
||||
widgetContainerComponent: SearchWidgetContainerComponent;
|
||||
|
||||
focusTrap: ConfigurableFocusTrap;
|
||||
|
||||
constructor(private focusTrapFactory: ConfigurableFocusTrapFactory) {}
|
||||
|
||||
onMenuOpen() {
|
||||
if (this.menuContainer && !this.focusTrap) {
|
||||
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
|
||||
this.focusTrap.focusInitialElement();
|
||||
}
|
||||
}
|
||||
|
||||
onClosed() {
|
||||
this.focusTrap.destroy();
|
||||
this.focusTrap = null;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this.widgetContainerComponent.resetInnerWidget();
|
||||
this.menuTrigger.closeMenu();
|
||||
}
|
||||
|
||||
onApply() {
|
||||
this.widgetContainerComponent.applyInnerWidget();
|
||||
this.menuTrigger.closeMenu();
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ import { Subject } from 'rxjs';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchService, setupTestBed, AlfrescoApiService } from '@alfresco/adf-core';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
import { SearchHeaderQueryBuilderService } from '../../services/search-header-query-builder.service';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { fakeNodePaging } from './../../../mock/document-list.component.mock';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
@@ -30,7 +30,7 @@ import {
|
||||
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
|
||||
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
||||
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
import { SearchHeaderQueryBuilderService } from '../../services/search-header-query-builder.service';
|
||||
import { SearchCategory } from '../../models/search-category.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<mat-accordion multi="true" displayMode="flat">
|
||||
|
||||
<button *ngIf="displayResetButton && responseFacets"
|
||||
<button *ngIf="displayResetButton && facetFiltersService.responseFacets"
|
||||
mat-button
|
||||
color="primary"
|
||||
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.TOOLTIP' | translate }}"
|
||||
matTooltipPosition="right"
|
||||
(click)="resetAll()">
|
||||
adf-reset-search>
|
||||
{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.LABEL' | translate }}
|
||||
</button>
|
||||
<mat-expansion-panel
|
||||
@@ -24,72 +24,15 @@
|
||||
</adf-search-widget-container>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<ng-container *ngIf="responseFacets && showContextFacets">
|
||||
<mat-expansion-panel [attr.data-automation-id]="'expansion-panel-'+field.label" *ngFor="let field of responseFacets"
|
||||
<ng-container *ngIf="facetFiltersService.responseFacets && showContextFacets">
|
||||
<mat-expansion-panel [attr.data-automation-id]="'expansion-panel-'+field.label" *ngFor="let field of facetFiltersService.responseFacets"
|
||||
[expanded]="shouldExpand(field)">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ field.label | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="adf-facet-result-filter">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'SEARCH.FILTER.ACTIONS.FILTER-CATEGORY' | translate }}"
|
||||
[attr.data-automation-id]="'facet-result-filter-'+field.label"
|
||||
[(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>
|
||||
<adf-search-facet-field [field]="field"></adf-search-facet-field>
|
||||
|
||||
<div class="adf-checklist">
|
||||
<mat-checkbox
|
||||
*ngFor="let bucket of field.buckets"
|
||||
[checked]="bucket.checked"
|
||||
[attr.data-automation-id]="'checkbox-'+field.label+'-'+(bucket.display || bucket.label)"
|
||||
(change)="onToggleBucket($event, field, bucket)">
|
||||
<div
|
||||
matTooltip="{{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }}"
|
||||
matTooltipPosition="right"
|
||||
class="adf-facet-label">
|
||||
{{ bucket.display || bucket.label | translate }} {{ getBucketCountDisplay(bucket) }}
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="adf-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="adf-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>
|
||||
|
@@ -2,57 +2,6 @@
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
.adf-search-filter {
|
||||
|
||||
.adf-checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mat-checkbox-label {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-facet-label {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mat-checkbox {
|
||||
margin: 5px;
|
||||
|
||||
&.mat-checkbox-checked .mat-checkbox-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-facet-result-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.adf-facet-buttons {
|
||||
text-align: right;
|
||||
|
||||
.mat-button {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--topSpace {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-title {
|
||||
font-size: 14px;
|
||||
color: mat-color($foreground, text, 0.87);
|
||||
|
@@ -16,11 +16,9 @@
|
||||
*/
|
||||
|
||||
import { SearchFilterComponent } from './search-filter.component';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { AppConfigService, SearchService, setupTestBed, TranslationService } from '@alfresco/adf-core';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { AppConfigService, SearchService, TranslationService } from '@alfresco/adf-core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
||||
import { FacetField } from '../../models/facet-field.interface';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -39,6 +37,8 @@ import {
|
||||
stepTwo
|
||||
} from '../../../mock';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
import { SearchFacetFieldComponent } from '../search-facet-field/search-facet-field.component';
|
||||
|
||||
describe('SearchFilterComponent', () => {
|
||||
let fixture: ComponentFixture<SearchFilterComponent>;
|
||||
@@ -48,18 +48,19 @@ describe('SearchFilterComponent', () => {
|
||||
const searchMock: any = {
|
||||
dataLoaded: new Subject()
|
||||
};
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchMock }
|
||||
]
|
||||
});
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchMock }
|
||||
]
|
||||
});
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
fixture = TestBed.createComponent(SearchFilterComponent);
|
||||
appConfigService = TestBed.inject(AppConfigService);
|
||||
@@ -73,346 +74,6 @@ describe('SearchFilterComponent', () => {
|
||||
describe('component', () => {
|
||||
beforeEach(() => fixture.detectChanges());
|
||||
|
||||
it('should subscribe to query builder executed event', () => {
|
||||
spyOn(component, 'onDataLoaded').and.stub();
|
||||
const data = { list: {} };
|
||||
queryBuilder.executed.next(data);
|
||||
|
||||
expect(component.onDataLoaded).toHaveBeenCalledWith(data);
|
||||
});
|
||||
|
||||
it('should update bucket model and query builder on facet toggle', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'addUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: true };
|
||||
const field: FacetField = { field: 'f1', label: 'f1' };
|
||||
const bucket: FacetFieldBucket = { checked: false, filterQuery: 'q1', label: 'q1', count: 1 };
|
||||
|
||||
component.onToggleBucket(event, field, bucket);
|
||||
|
||||
expect(bucket.checked).toBeTruthy();
|
||||
expect(queryBuilder.addUserFacetBucket).toHaveBeenCalledWith(field, bucket);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update bucket model and query builder on facet un-toggle', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: false };
|
||||
const field: FacetField = { field: 'f1', label: 'f1' };
|
||||
const bucket: FacetFieldBucket = { checked: true, filterQuery: 'q1', label: 'q1', count: 1 };
|
||||
|
||||
component.onToggleBucket(event, field, bucket);
|
||||
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, bucket);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unselect facet query and update builder', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(queryBuilder, 'removeUserFacetBucket').and.callThrough();
|
||||
|
||||
const event: any = { checked: false };
|
||||
const query = { checked: true, label: 'q1', filterQuery: 'query1' };
|
||||
const field = { type: 'query', label: 'label1', buckets: [ query ] };
|
||||
|
||||
component.onToggleBucket(event, <any> field, <any> query);
|
||||
|
||||
expect(query.checked).toEqual(false);
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledWith(field, query);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch facet queries from response payload', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
label: 'label1',
|
||||
queries: [
|
||||
{ label: 'q1', query: 'query1' },
|
||||
{ label: 'q2', query: 'query2' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const queries = [
|
||||
{ label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: [{
|
||||
type: 'query',
|
||||
label: 'label1',
|
||||
buckets: queries
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
|
||||
expect(component.responseFacets.length).toBe(1);
|
||||
expect(component.responseFacets[0].buckets.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should preserve order after response processing', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
label: 'label1',
|
||||
queries: [
|
||||
{ label: 'q1', query: 'query1' },
|
||||
{ label: 'q2', query: 'query2' },
|
||||
{ label: 'q3', query: 'query3' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const queries = [
|
||||
{ label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q3', filterQuery: 'query3', metrics: [{value: {count: 1}}] }
|
||||
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: [{
|
||||
type: 'query',
|
||||
label: 'label1',
|
||||
buckets: queries
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
|
||||
expect(component.responseFacets.length).toBe(1);
|
||||
expect(component.responseFacets[0].buckets.length).toBe(3);
|
||||
expect(component.responseFacets[0].buckets.items[0].label).toBe('q1');
|
||||
expect(component.responseFacets[0].buckets.items[1].label).toBe('q2');
|
||||
expect(component.responseFacets[0].buckets.items[2].label).toBe('q3');
|
||||
});
|
||||
|
||||
it('should not fetch facet queries from response payload', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
|
||||
expect(component.responseFacets).toBeNull();
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1', mincount: 0 },
|
||||
{ label: 'f2', field: 'f2', mincount: 0 }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const fields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: [{ label: 'a1' }, { label: 'a2' }] },
|
||||
{ type: 'field', label: 'f2', buckets: [{ label: 'b1' }, { label: 'b2' }] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: fields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
|
||||
expect(component.responseFacets.length).toEqual(2);
|
||||
expect(component.responseFacets[0].buckets.length).toEqual(2);
|
||||
expect(component.responseFacets[1].buckets.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should filter response facet fields based on search filter config method', () => {
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
},
|
||||
filterWithContains: false
|
||||
};
|
||||
|
||||
const initialFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: [
|
||||
{ label: 'firstLabel', display: 'firstLabel', metrics: [{value: {count: 5}}] },
|
||||
{ label: 'secondLabel', display: 'secondLabel', metrics: [{value: {count: 5}}] },
|
||||
{ label: 'thirdLabel', display: 'thirdLabel', metrics: [{value: {count: 5}}] }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: initialFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
expect(component.responseFacets.length).toBe(1);
|
||||
expect(component.responseFacets[0].buckets.visibleItems.length).toBe(3);
|
||||
|
||||
component.responseFacets[0].buckets.filterText = 'f';
|
||||
expect(component.responseFacets[0].buckets.visibleItems.length).toBe(1);
|
||||
expect(component.responseFacets[0].buckets.visibleItems[0].label).toEqual('firstLabel');
|
||||
|
||||
component.responseFacets[0].buckets.filterText = 'label';
|
||||
expect(component.responseFacets[0].buckets.visibleItems.length).toBe(0);
|
||||
|
||||
// Set filter method to use contains and test again
|
||||
queryBuilder.config.filterWithContains = true;
|
||||
component.responseFacets[0].buckets.filterText = 'f';
|
||||
expect(component.responseFacets[0].buckets.visibleItems.length).toBe(1);
|
||||
component.responseFacets[0].buckets.filterText = 'label';
|
||||
expect(component.responseFacets[0].buckets.visibleItems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the bucket values', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [
|
||||
{ label: 'b1', metrics: [{value: {count: 10}}] },
|
||||
{ label: 'b2', metrics: [{value: {count: 1}}] }
|
||||
]
|
||||
},
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
expect(component.responseFacets.length).toEqual(1);
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
expect(component.responseFacets[0].buckets.items[1].count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and update the existing bucket values', () => {
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const initialFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: { items: [{ label: 'b1', count: 10, filterQuery: 'filter' }, { label: 'b2', count: 1 }]} },
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
component.responseFacets = initialFields;
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
expect(component.responseFacets[0].buckets.items[1].count).toEqual(1);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets:
|
||||
[{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' },
|
||||
{ label: 'b2', metrics: [{value: {count: 0}}] }] },
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.onDataLoaded(data);
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(6);
|
||||
expect(component.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update correctly the existing facetFields bucket values', () => {
|
||||
component.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [{ label: 'f1', field: 'f1' }] },
|
||||
facetQueries: { queries: [] }
|
||||
};
|
||||
|
||||
const firstCallFields: any = [{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [{ label: 'b1', metrics: [{value: {count: 10}}] }]
|
||||
}];
|
||||
const firstCallData = { list: { context: { facets: firstCallFields }}};
|
||||
component.onDataLoaded(firstCallData);
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
|
||||
const secondCallFields: any = [{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [{ label: 'b1', metrics: [{value: {count: 6}}] }]
|
||||
}];
|
||||
const secondCallData = { list: { context: { facets: secondCallFields}}};
|
||||
component.onDataLoaded(secondCallData);
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(6);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the already checked items', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
queryBuilder.config = {
|
||||
@@ -426,13 +87,13 @@ describe('SearchFilterComponent', () => {
|
||||
}
|
||||
};
|
||||
|
||||
component.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
||||
searchFacetFiltersService.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) }
|
||||
];
|
||||
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
|
||||
queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
||||
@@ -447,10 +108,14 @@ describe('SearchFilterComponent', () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
component.selectFacetBucket({ field: 'f1', label: 'f1' }, component.responseFacets[0].buckets.items[1]);
|
||||
component.onDataLoaded(data);
|
||||
expect(component.responseFacets.length).toEqual(2);
|
||||
expect(component.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item');
|
||||
|
||||
fixture.detectChanges();
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item');
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the newly checked items', () => {
|
||||
@@ -466,13 +131,13 @@ describe('SearchFilterComponent', () => {
|
||||
}
|
||||
};
|
||||
|
||||
component.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
||||
searchFacetFiltersService.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList([]) }
|
||||
];
|
||||
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
|
||||
searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
||||
@@ -487,10 +152,14 @@ describe('SearchFilterComponent', () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
component.selectFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[1]);
|
||||
component.onDataLoaded(data);
|
||||
expect(component.responseFacets.length).toEqual(2);
|
||||
expect(component.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item');
|
||||
|
||||
fixture.detectChanges();
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ field: 'f1', label: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item');
|
||||
});
|
||||
|
||||
it('should show buckets with 0 values when there are no facet fields on the response payload', () => {
|
||||
@@ -506,99 +175,26 @@ describe('SearchFilterComponent', () => {
|
||||
}
|
||||
};
|
||||
|
||||
component.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
||||
searchFacetFiltersService.responseFacets = <any> [
|
||||
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||
{ type: 'field', label: 'f2', field: 'f2', buckets: new SearchFilterList() }
|
||||
];
|
||||
component.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[0]);
|
||||
searchFacetFiltersService.queryBuilder.addUserFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[0]);
|
||||
const data = {
|
||||
list: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
component.selectFacetBucket({ label: 'f1', field: 'f1' }, component.responseFacets[0].buckets.items[1]);
|
||||
component.onDataLoaded(data);
|
||||
|
||||
expect(component.responseFacets[0].buckets.items[0].count).toEqual(0);
|
||||
expect(component.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.selectFacetBucket({ label: 'f1', field: 'f1' }, searchFacetFiltersService.responseFacets[0].buckets.items[1]);
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
it('should update query builder only when has bucket to unselect', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
const field: FacetField = { field: 'f1', label: 'f1' };
|
||||
component.onToggleBucket(<any> { checked: true }, field, null);
|
||||
|
||||
expect(queryBuilder.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow to to reset selected buckets', () => {
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: true, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
expect(component.canResetSelectedBuckets(field)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not allow to reset selected buckets', () => {
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: false, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
expect(component.canResetSelectedBuckets(field)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should reset selected buckets', () => {
|
||||
spyOn(queryBuilder, 'execute').and.stub();
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.resetSelectedBuckets(field);
|
||||
|
||||
expect(buckets[0].checked).toEqual(false);
|
||||
expect(buckets[1].checked).toEqual(false);
|
||||
});
|
||||
|
||||
it('should update query builder upon resetting buckets', () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
|
||||
const buckets: FacetFieldBucket[] = [
|
||||
{ label: 'bucket1', checked: false, count: 1, filterQuery: 'q1' },
|
||||
{ label: 'bucket2', checked: true, count: 1, filterQuery: 'q2' }
|
||||
];
|
||||
|
||||
const field: FacetField = {
|
||||
field: 'f1',
|
||||
label: 'field1',
|
||||
buckets: new SearchFilterList<FacetFieldBucket>(buckets)
|
||||
};
|
||||
|
||||
component.resetSelectedBuckets(field);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(0);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update query builder upon resetting selected queries', () => {
|
||||
@@ -607,110 +203,25 @@ describe('SearchFilterComponent', () => {
|
||||
|
||||
const queryResponse = <any> {
|
||||
label: 'query response',
|
||||
buckets: <any> {
|
||||
items: [
|
||||
buckets: new SearchFilterList([
|
||||
{ label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }]
|
||||
}};
|
||||
component.responseFacets = [queryResponse];
|
||||
component.resetSelectedBuckets(queryResponse);
|
||||
{ label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }])
|
||||
};
|
||||
searchFacetFiltersService.responseFacets = [queryResponse];
|
||||
|
||||
fixture.detectChanges();
|
||||
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||
facetField.resetSelectedBuckets(queryResponse);
|
||||
|
||||
expect(queryBuilder.removeUserFacetBucket).toHaveBeenCalledTimes(3);
|
||||
expect(queryBuilder.update).toHaveBeenCalled();
|
||||
|
||||
for (const entry of component.responseFacets[0].buckets.items) {
|
||||
for (const entry of searchFacetFiltersService.responseFacets[0].buckets.items) {
|
||||
expect(entry.checked).toEqual(false);
|
||||
}
|
||||
});
|
||||
|
||||
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(1);
|
||||
expect(component.responseFacets[0].buckets.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('widgets', () => {
|
||||
|
||||
@@ -919,8 +430,6 @@ describe('SearchFilterComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(component, 'selectFacetBucket').and.callThrough();
|
||||
spyOn(component, 'onToggleBucket').and.callThrough();
|
||||
|
||||
const inputElement = fixture.debugElement.query(By.css(`${panel} input`));
|
||||
inputElement.nativeElement.value = 'Extra';
|
||||
@@ -937,7 +446,7 @@ describe('SearchFilterComponent', () => {
|
||||
filteredMenu = getAllMenus(`${panel} mat-checkbox`, fixture);
|
||||
expect(filteredMenu).toEqual(filteredResult);
|
||||
|
||||
const clearButton = fixture.debugElement.query(By.css(`${panel} button`));
|
||||
const clearButton = fixture.debugElement.query(By.css(`${panel} mat-form-field button`));
|
||||
clearButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -951,8 +460,7 @@ describe('SearchFilterComponent', () => {
|
||||
const checkedOption = fixture.debugElement.query(By.css(`${panel} mat-checkbox.mat-checkbox-checked`));
|
||||
expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)');
|
||||
|
||||
expect(component.onToggleBucket).toHaveBeenCalledTimes(1);
|
||||
expect(component.selectFacetBucket).toHaveBeenCalledTimes(1);
|
||||
expect(queryBuilder.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should preserve the filter state if other fields edited', () => {
|
||||
@@ -964,8 +472,6 @@ describe('SearchFilterComponent', () => {
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
spyOn(component, 'selectFacetBucket').and.callThrough();
|
||||
spyOn(component, 'onToggleBucket').and.callThrough();
|
||||
|
||||
const inputElement = fixture.debugElement.query(By.css(`${panel1} input`));
|
||||
inputElement.nativeElement.value = 'my';
|
||||
@@ -995,15 +501,20 @@ describe('SearchFilterComponent', () => {
|
||||
panel1CheckedOption = fixture.debugElement.query(By.css(`${panel1} mat-checkbox.mat-checkbox-checked`));
|
||||
expect(panel1CheckedOption.nativeElement.innerText).toEqual('my1 (806)');
|
||||
|
||||
expect(component.onToggleBucket).toHaveBeenCalledTimes(2);
|
||||
expect(component.selectFacetBucket).toHaveBeenCalledTimes(2);
|
||||
expect(queryBuilder.update).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should reset the query fragments when reset All is clicked', () => {
|
||||
component.queryBuilder.queryFragments = { 'fragment1' : 'value1'};
|
||||
component.responseFacets = [];
|
||||
spyOn(queryBuilder, 'resetToDefaults').and.stub();
|
||||
component.resetAll();
|
||||
appConfigService.config.search = searchFilter;
|
||||
searchFacetFiltersService.responseFacets = [];
|
||||
component.displayResetButton = true;
|
||||
fixture.detectChanges();
|
||||
spyOn(queryBuilder, 'resetToDefaults').and.callThrough();
|
||||
|
||||
const resetButton = fixture.debugElement.query(By.css('button'));
|
||||
resetButton.nativeElement.click();
|
||||
|
||||
expect(component.queryBuilder.queryFragments).toEqual({});
|
||||
expect(queryBuilder.resetToDefaults).toHaveBeenCalled();
|
||||
});
|
||||
|
@@ -15,22 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, OnDestroy, Inject, Input } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { TranslationService, SearchService } from '@alfresco/adf-core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { Component, Inject, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
||||
import { FacetField } from '../../models/facet-field.interface';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
export interface SelectedBucket {
|
||||
field: FacetField;
|
||||
bucket: FacetFieldBucket;
|
||||
}
|
||||
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-filter',
|
||||
@@ -38,36 +28,22 @@ export interface SelectedBucket {
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'adf-search-filter' }
|
||||
})
|
||||
export class SearchFilterComponent implements OnInit, OnDestroy {
|
||||
export class SearchFilterComponent {
|
||||
|
||||
/** Toggles whether to show or not the context facet filters. */
|
||||
@Input()
|
||||
showContextFacets: boolean = true;
|
||||
|
||||
private DEFAULT_PAGE_SIZE = 5;
|
||||
|
||||
/** All facet field items to be displayed in the component. These are updated according to the response.
|
||||
* When a new search is performed, the already existing items are updated with the new bucket count values and
|
||||
* the newly received items are added to the responseFacets.
|
||||
*/
|
||||
responseFacets: FacetField[] = null;
|
||||
|
||||
private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
|
||||
facetQueriesLabel: string = 'Facet Queries';
|
||||
facetExpanded = {
|
||||
'default': false
|
||||
};
|
||||
displayResetButton: boolean;
|
||||
selectedBuckets: SelectedBucket[] = [];
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
||||
private searchService: SearchService,
|
||||
private translationService: TranslationService) {
|
||||
public facetFiltersService: SearchFacetFiltersService) {
|
||||
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.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded;
|
||||
}
|
||||
if (queryBuilder.config && queryBuilder.config.facetFields) {
|
||||
@@ -77,350 +53,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
|
||||
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
|
||||
}
|
||||
this.displayResetButton = this.queryBuilder.config && !!this.queryBuilder.config.resetButton;
|
||||
|
||||
this.queryBuilder.updated
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((query) => this.queryBuilder.execute(query));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.queryBuilder) {
|
||||
this.queryBuilder.executed
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((resultSetPaging: ResultSetPaging) => {
|
||||
this.onDataLoaded(resultSetPaging);
|
||||
this.searchService.dataLoaded.next(resultSetPaging);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
private updateSelectedBuckets() {
|
||||
if (this.responseFacets) {
|
||||
this.selectedBuckets = [];
|
||||
for (const field of this.responseFacets) {
|
||||
if (field.buckets) {
|
||||
this.selectedBuckets.push(
|
||||
...this.queryBuilder.getUserFacetBuckets(field.field)
|
||||
.filter((bucket) => bucket.checked)
|
||||
.map((bucket) => {
|
||||
return { field, bucket };
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedBuckets = [];
|
||||
}
|
||||
}
|
||||
|
||||
onToggleBucket(event: MatCheckboxChange, field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (event && bucket) {
|
||||
if (event.checked) {
|
||||
this.selectFacetBucket(field, bucket);
|
||||
} else {
|
||||
this.unselectFacetBucket(field, bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (bucket) {
|
||||
bucket.checked = true;
|
||||
this.queryBuilder.addUserFacetBucket(field, bucket);
|
||||
this.updateSelectedBuckets();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
|
||||
unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (bucket) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
this.updateSelectedBuckets();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
|
||||
canResetSelectedBuckets(field: FacetField): boolean {
|
||||
if (field && field.buckets) {
|
||||
return field.buckets.items.some((bucket) => bucket.checked);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resetSelectedBuckets(field: FacetField) {
|
||||
if (field && field.buckets) {
|
||||
for (const bucket of field.buckets.items) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
}
|
||||
this.updateSelectedBuckets();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
|
||||
resetAllSelectedBuckets() {
|
||||
this.responseFacets.forEach((field) => {
|
||||
if (field && field.buckets) {
|
||||
for (const bucket of field.buckets.items) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
}
|
||||
this.updateSelectedBuckets();
|
||||
}
|
||||
});
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
|
||||
resetQueryFragments() {
|
||||
this.queryBuilder.queryFragments = {};
|
||||
this.queryBuilder.resetToDefaults();
|
||||
}
|
||||
|
||||
resetAll() {
|
||||
this.resetAllSelectedBuckets();
|
||||
this.resetQueryFragments();
|
||||
this.responseFacets = null;
|
||||
}
|
||||
|
||||
shouldExpand(field: FacetField): boolean {
|
||||
return this.facetExpanded[field.type] || this.facetExpanded['default'];
|
||||
}
|
||||
|
||||
onDataLoaded(data: any) {
|
||||
const context = data.list.context;
|
||||
|
||||
if (context) {
|
||||
this.parseFacets(context);
|
||||
} else {
|
||||
this.responseFacets = null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseFacets(context: ResultSetContext) {
|
||||
this.parseFacetFields(context);
|
||||
this.parseFacetIntervals(context);
|
||||
this.parseFacetQueries(context);
|
||||
}
|
||||
|
||||
private parseFacetItems(context: ResultSetContext, configFacetFields: FacetField[], itemType: string) {
|
||||
configFacetFields.forEach((field) => {
|
||||
const responseField = this.findFacet(context, itemType, field.label);
|
||||
const responseBuckets = this.getResponseBuckets(responseField, field)
|
||||
.filter(this.getFilterByMinCount(field.mincount));
|
||||
const alreadyExistingField = this.findResponseFacet(itemType, field.label);
|
||||
|
||||
if (alreadyExistingField) {
|
||||
const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
|
||||
|
||||
this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
|
||||
} else if (responseField && this.showContextFacets) {
|
||||
if (responseBuckets.length > 0) {
|
||||
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, field.pageSize);
|
||||
bucketList.filter = this.getBucketFilterFunction(bucketList);
|
||||
|
||||
if (!this.responseFacets) {
|
||||
this.responseFacets = [];
|
||||
}
|
||||
this.responseFacets.push(<FacetField> {
|
||||
...field,
|
||||
type: responseField.type || itemType,
|
||||
label: field.label,
|
||||
pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
|
||||
currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
|
||||
buckets: bucketList
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private parseFacetFields(context: ResultSetContext) {
|
||||
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
|
||||
this.parseFacetItems(context, configFacetFields, 'field');
|
||||
}
|
||||
|
||||
private parseFacetIntervals(context: ResultSetContext) {
|
||||
const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || [];
|
||||
this.parseFacetItems(context, configFacetIntervals, 'interval');
|
||||
}
|
||||
|
||||
private parseFacetQueries(context: ResultSetContext) {
|
||||
const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || [];
|
||||
const configGroups = configFacetQueries.reduce((acc, query) => {
|
||||
const group = this.queryBuilder.getQueryGroup(query);
|
||||
if (acc[group]) {
|
||||
acc[group].push(query);
|
||||
} else {
|
||||
acc[group] = [query];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount;
|
||||
const mincountFilter = this.getFilterByMinCount(mincount);
|
||||
|
||||
Object.keys(configGroups).forEach((group) => {
|
||||
const responseField = this.findFacet(context, 'query', group);
|
||||
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group])
|
||||
.filter(mincountFilter);
|
||||
const alreadyExistingField = this.findResponseFacet('query', group);
|
||||
|
||||
if (alreadyExistingField) {
|
||||
const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
|
||||
|
||||
this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
|
||||
} else if (responseField && this.showContextFacets) {
|
||||
if (responseBuckets.length > 0) {
|
||||
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, this.facetQueriesPageSize);
|
||||
bucketList.filter = this.getBucketFilterFunction(bucketList);
|
||||
|
||||
if (!this.responseFacets) {
|
||||
this.responseFacets = [];
|
||||
}
|
||||
this.responseFacets.push(<FacetField> {
|
||||
field: group,
|
||||
type: responseField.type || 'query',
|
||||
label: group,
|
||||
pageSize: this.DEFAULT_PAGE_SIZE,
|
||||
currentPageSize: this.DEFAULT_PAGE_SIZE,
|
||||
buckets: bucketList
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private getResponseBuckets(responseField: GenericFacetResponse, configField: FacetField): FacetFieldBucket[] {
|
||||
return ((responseField && responseField.buckets) || []).map((respBucket) => {
|
||||
|
||||
respBucket['count'] = this.getCountValue(respBucket);
|
||||
respBucket.filterQuery = respBucket.filterQuery || this.getCorrespondingFilterQuery(configField, respBucket.label);
|
||||
return <FacetFieldBucket> {
|
||||
...respBucket,
|
||||
checked: false,
|
||||
display: respBucket.display,
|
||||
label: respBucket.label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getResponseQueryBuckets(responseField: GenericFacetResponse, configGroup: any): FacetFieldBucket[] {
|
||||
return (configGroup || []).map((query) => {
|
||||
const respBucket = ((responseField && responseField.buckets) || [])
|
||||
.find((bucket) => bucket.label === query.label) || {};
|
||||
|
||||
respBucket['count'] = this.getCountValue(respBucket);
|
||||
return <FacetFieldBucket> {
|
||||
...respBucket,
|
||||
checked: false,
|
||||
display: respBucket.display,
|
||||
label: respBucket.label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getCountValue(bucket: GenericBucket): number {
|
||||
return (!!bucket && !!bucket.metrics && bucket.metrics[0]?.value?.count) || 0;
|
||||
}
|
||||
|
||||
getBucketCountDisplay(bucket: FacetFieldBucket): string {
|
||||
return bucket.count === null ? '' : `(${bucket.count})`;
|
||||
}
|
||||
|
||||
private getFilterByMinCount(mincountInput: number) {
|
||||
return (bucket) => {
|
||||
let mincount = mincountInput;
|
||||
if (mincount === undefined) {
|
||||
mincount = 1;
|
||||
}
|
||||
return bucket.count >= mincount;
|
||||
};
|
||||
}
|
||||
|
||||
private getCorrespondingFilterQuery(configFacetItem: FacetField, bucketLabel: string): string {
|
||||
let filterQuery = null;
|
||||
|
||||
if (configFacetItem.field && bucketLabel) {
|
||||
|
||||
if (configFacetItem.sets) {
|
||||
const configSet = configFacetItem.sets.find((set) => bucketLabel === set.label);
|
||||
|
||||
if (configSet) {
|
||||
filterQuery = this.buildIntervalQuery(configFacetItem.field, configSet);
|
||||
}
|
||||
|
||||
} else {
|
||||
filterQuery = `${configFacetItem.field}:"${bucketLabel}"`;
|
||||
}
|
||||
}
|
||||
|
||||
return filterQuery;
|
||||
}
|
||||
|
||||
private buildIntervalQuery(fieldName: string, interval: any): string {
|
||||
const start = interval.start;
|
||||
const end = interval.end;
|
||||
const startLimit = (interval.startInclusive === undefined || interval.startInclusive === true) ? '[' : '<';
|
||||
const endLimit = (interval.endInclusive === undefined || interval.endInclusive === true) ? ']' : '>';
|
||||
|
||||
return `${fieldName}:${startLimit}"${start}" TO "${end}"${endLimit}`;
|
||||
}
|
||||
|
||||
private findFacet(context: ResultSetContext, itemType: string, fieldLabel: string): GenericFacetResponse {
|
||||
return (context.facets || []).find((response) => response.type === itemType && response.label === fieldLabel) || {};
|
||||
}
|
||||
|
||||
private findResponseFacet(itemType: string, fieldLabel: string): FacetField {
|
||||
return (this.responseFacets || []).find((response) => response.type === itemType && response.label === fieldLabel);
|
||||
}
|
||||
|
||||
private updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets) {
|
||||
const bucketsToDelete = [];
|
||||
|
||||
alreadyExistingBuckets
|
||||
.map((bucket) => {
|
||||
const responseBucket = ((responseField && responseField.buckets) || []).find((respBucket) => respBucket.label === bucket.label);
|
||||
|
||||
if (!responseBucket) {
|
||||
bucketsToDelete.push(bucket);
|
||||
}
|
||||
bucket.count = this.getCountValue(responseBucket);
|
||||
return bucket;
|
||||
});
|
||||
|
||||
const hasSelection = this.selectedBuckets
|
||||
.find((selBuckets) => alreadyExistingField.label === selBuckets.field.label && alreadyExistingField.type === selBuckets.field.type);
|
||||
|
||||
if (!hasSelection && bucketsToDelete.length) {
|
||||
bucketsToDelete.forEach((bucket) => {
|
||||
alreadyExistingField.buckets.deleteItem(bucket);
|
||||
});
|
||||
}
|
||||
|
||||
responseBuckets.forEach((respBucket) => {
|
||||
const existingBucket = alreadyExistingBuckets.find((oldBucket) => oldBucket.label === respBucket.label);
|
||||
|
||||
if (!existingBucket) {
|
||||
alreadyExistingField.buckets.addItem(respBucket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getBucketFilterFunction(bucketList) {
|
||||
return (bucket: FacetFieldBucket): boolean => {
|
||||
if (bucket && bucketList.filterText) {
|
||||
const pattern = (bucketList.filterText || '').toLowerCase();
|
||||
const label = (this.translationService.instant(bucket.display) || this.translationService.instant(bucket.label)).toLowerCase();
|
||||
return this.queryBuilder.config.filterWithContains ? label.indexOf(pattern) !== -1 : label.startsWith(pattern);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,33 @@
|
||||
<mat-form-field floatLabel="always" *ngIf="searchForms.length">
|
||||
<mat-label>{{ 'SEARCH.FORMS' | translate }}</mat-label>
|
||||
<mat-select [(value)]="selected" (selectionChange)="onSelectionChange($event.value)">
|
||||
<mat-option *ngFor="let form of searchForms" [value]="form.index">
|
||||
{{form.name | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="queryBuilder.searchForms | async as forms">
|
||||
|
||||
<ng-container *ngIf="forms.length === 1">
|
||||
<button class="adf-search-form" disableRipple mat-button [title]="getSelected(forms) | translate">
|
||||
<span class="adf-search-form-title">
|
||||
{{ getSelected(forms) | translate }}
|
||||
</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="forms.length > 1">
|
||||
<button class="adf-search-form"
|
||||
[matMenuTriggerFor]="menu"
|
||||
#menuTrigger="matMenuTrigger"
|
||||
disableRipple
|
||||
mat-button
|
||||
[title]="getSelected(forms) | translate"
|
||||
[matMenuTriggerRestoreFocus]="true">
|
||||
|
||||
<span class="adf-search-form-title" >
|
||||
{{ getSelected(forms) | translate }}
|
||||
</span>
|
||||
|
||||
<mat-icon [class.adf-search-form-icon-selected]="menuTrigger.menuOpen" class="adf-search-form-icon">expand_more</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu" backdropClass="adf-search-form-menu">
|
||||
<button mat-menu-item *ngFor="let form of forms" (click)="onSelectionChange(form)">
|
||||
{{form.name | translate}}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@@ -0,0 +1,49 @@
|
||||
@mixin adf-search-forms-theme($theme) {
|
||||
$accent: map-get($theme, accent);
|
||||
|
||||
.adf-search-form {
|
||||
&.mat-button {
|
||||
height: 35px;
|
||||
max-width: 190px;
|
||||
min-width: 190px;
|
||||
align-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.mat-button-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
max-width: 120px;
|
||||
min-width: 120px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding-right: 12px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
transition: border 500ms ease-out;
|
||||
}
|
||||
|
||||
&-icon-selected {
|
||||
border-color: mat-color($accent);
|
||||
}
|
||||
|
||||
&-menu + * .mat-menu-panel {
|
||||
@include mat-elevation(2);
|
||||
border-radius: 6px;
|
||||
|
||||
.mat-menu-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,20 +16,19 @@
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchFormComponent } from './search-form.component';
|
||||
import { setupTestBed } from '@alfresco/adf-core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchForm } from '../../models/search-form.interface';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('SearchFormComponent', () => {
|
||||
let fixture: ComponentFixture<SearchFormComponent>;
|
||||
let component: SearchFormComponent;
|
||||
let queryBuilder: SearchHeaderQueryBuilderService;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
const mockSearchForms: SearchForm[] = [
|
||||
{ default: false, index: 0, name: 'All', selected: false },
|
||||
{ default: true, index: 1, name: 'First', selected: true },
|
||||
@@ -42,50 +41,59 @@ describe('SearchFormComponent', () => {
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService }
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchQueryBuilderService }
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
queryBuilder = TestBed.inject<SearchHeaderQueryBuilderService>(SEARCH_QUERY_SERVICE_TOKEN);
|
||||
spyOn(queryBuilder, 'getSearchConfigurationDetails').and.returnValue(mockSearchForms);
|
||||
queryBuilder = TestBed.inject<SearchQueryBuilderService>(SEARCH_QUERY_SERVICE_TOKEN);
|
||||
queryBuilder.searchForms.next(mockSearchForms);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show search forms', async () => {
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
expect(component.selected).toBe(1);
|
||||
const label = fixture.debugElement.query(By.css('.mat-form-field mat-label'));
|
||||
expect(label.nativeElement.innerText).toContain('SEARCH.FORMS');
|
||||
const selectValue = fixture.debugElement.query(By.css('.mat-select-value'));
|
||||
expect(selectValue.nativeElement.innerText).toContain('First');
|
||||
it('should show search forms', () => {
|
||||
const title = fixture.debugElement.query(By.css('.adf-search-form-title'));
|
||||
expect(title.nativeElement.innerText).toContain(mockSearchForms[1].name);
|
||||
});
|
||||
|
||||
it('should emit on form change', async (done) => {
|
||||
it('should emit on form change', (done) => {
|
||||
spyOn(queryBuilder, 'updateSelectedConfiguration').and.stub();
|
||||
component.formChange.subscribe((form) => {
|
||||
expect(form).toEqual(mockSearchForms[2]);
|
||||
expect(queryBuilder.updateSelectedConfiguration).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
await fixture.whenStable();
|
||||
const button = fixture.debugElement.query(By.css('.adf-search-form')).nativeElement;
|
||||
button.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const matSelect = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
matSelect.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const matOption = fixture.debugElement.queryAll(By.css('.mat-option'))[2].nativeElement;
|
||||
const matOption = fixture.debugElement.queryAll(By.css('.mat-menu-item'))[2].nativeElement;
|
||||
matOption.click();
|
||||
});
|
||||
|
||||
it('should not display search form if no form configured', async () => {
|
||||
component.searchForms = [];
|
||||
await fixture.whenStable();
|
||||
it('should not show menu if only one config found', () => {
|
||||
queryBuilder.searchForms.next([{ name: 'one', selected: true, default: true, index: 0 }]);
|
||||
fixture.detectChanges();
|
||||
const field = fixture.debugElement.query(By.css('.mat-form-field'));
|
||||
|
||||
const button = fixture.debugElement.query(By.css('.adf-search-form')).nativeElement;
|
||||
button.click();
|
||||
|
||||
const title = fixture.debugElement.query(By.css('.adf-search-form-title'));
|
||||
expect(title.nativeElement.innerText).toContain('one');
|
||||
|
||||
fixture.detectChanges();
|
||||
const matOption = fixture.debugElement.query(By.css('.mat-menu-item'));
|
||||
expect(matOption).toBe(null, 'should not show mat menu');
|
||||
});
|
||||
|
||||
it('should not display search form if no form configured', () => {
|
||||
queryBuilder.searchForms.next([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const field = fixture.debugElement.query(By.css('.adf-search-form-title'));
|
||||
expect(field).toEqual(null, 'search form displayed for empty configuration');
|
||||
});
|
||||
});
|
||||
|
@@ -15,30 +15,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { Component, EventEmitter, Inject, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchForm } from '../../models/search-form.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-form',
|
||||
templateUrl: './search-form.component.html'
|
||||
templateUrl: './search-form.component.html',
|
||||
styleUrls: ['./search-form.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
export class SearchFormComponent {
|
||||
@Output()
|
||||
formChange: EventEmitter<SearchForm> = new EventEmitter<SearchForm>();
|
||||
|
||||
selected: number;
|
||||
searchForms: SearchForm[] = [];
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchForms = this.queryBuilder.getSearchConfigurationDetails();
|
||||
this.selected = this.searchForms.find(form => form.selected)?.index;
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService) {
|
||||
}
|
||||
|
||||
onSelectionChange(index: number) {
|
||||
this.formChange.emit(this.searchForms[index]);
|
||||
onSelectionChange(form: SearchForm) {
|
||||
this.queryBuilder.updateSelectedConfiguration(form.index);
|
||||
this.formChange.emit(form);
|
||||
}
|
||||
|
||||
getSelected(forms: SearchForm[]): string {
|
||||
return forms.find((form) => form.selected)?.name;
|
||||
}
|
||||
}
|
||||
|
@@ -29,8 +29,8 @@
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<div class="adf-facet-buttons">
|
||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="number-range-btn-clear">
|
||||
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="number-range-btn-clear">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="number-range-btn-apply">
|
||||
|
@@ -15,12 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OnInit, Component, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-number-range',
|
||||
@@ -48,6 +49,8 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
||||
startValue: any;
|
||||
|
||||
validators: Validators;
|
||||
enableChangeUpdate: boolean;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -74,6 +77,9 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
||||
from: this.from,
|
||||
to: this.to
|
||||
}, this.formValidator);
|
||||
|
||||
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||
this.updateDisplayValue();
|
||||
}
|
||||
|
||||
formValidator(formGroup: FormGroup) {
|
||||
@@ -82,6 +88,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
||||
|
||||
apply(model: { from: string, to: string }, isValid: boolean) {
|
||||
if (isValid && this.id && this.context && this.field) {
|
||||
this.updateDisplayValue();
|
||||
this.isActive = true;
|
||||
|
||||
const map = new Map<string, string>();
|
||||
@@ -118,12 +125,21 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
||||
return this.form.value;
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
if (this.form.invalid || this.form.pristine) {
|
||||
this.displayValue$.next('');
|
||||
} else {
|
||||
this.displayValue$.next(`${this.form.value.from} - ${this.form.value.to} ${this.settings.unit ?? ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: any) {
|
||||
this.form['from'].setValue(value);
|
||||
this.form['to'].setValue(value);
|
||||
this.updateDisplayValue();
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
|
||||
this.form.reset({
|
||||
@@ -133,6 +149,16 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
||||
|
||||
if (this.id && this.context) {
|
||||
this.context.queryFragments[this.id] = '';
|
||||
this.updateDisplayValue();
|
||||
if (this.enableChangeUpdate) {
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clear();
|
||||
if (this.id && this.context) {
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ContentNodeSelectorPanelService } from '../../../content-node-selector/content-node-selector-panel.service';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
@Component({
|
||||
|
@@ -15,13 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { MatRadioChange } from '@angular/material/radio';
|
||||
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchFilterList } from '../../models/search-filter-list.model';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface SearchRadioOption {
|
||||
name: string;
|
||||
@@ -48,6 +49,8 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
||||
pageSize = 5;
|
||||
isActive = false;
|
||||
startValue: any;
|
||||
enableChangeUpdate: boolean;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
constructor() {
|
||||
this.options = new SearchFilterList<SearchRadioOption>();
|
||||
@@ -73,6 +76,8 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
||||
this.value = initialValue;
|
||||
this.context.queryFragments[this.id] = initialValue;
|
||||
}
|
||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||
this.updateDisplayValue();
|
||||
}
|
||||
|
||||
private getSelectedValue(): string {
|
||||
@@ -90,8 +95,9 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
|
||||
submitValues() {
|
||||
const currentValue = this.getSelectedValue();
|
||||
this.setValue(currentValue);
|
||||
this.setValue(this.value);
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
|
||||
hasValidValue() {
|
||||
@@ -102,23 +108,43 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
||||
setValue(newValue: string) {
|
||||
this.value = newValue;
|
||||
this.context.queryFragments[this.id] = newValue;
|
||||
this.context.update();
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentValue() {
|
||||
return this.getSelectedValue();
|
||||
}
|
||||
|
||||
updateDisplayValue(): void {
|
||||
const selectOptions = this.options.items.find(({ value}) => value === this.value);
|
||||
if (selectOptions) {
|
||||
this.displayValue$.next(selectOptions.name);
|
||||
} else {
|
||||
this.displayValue$.next('');
|
||||
}
|
||||
}
|
||||
|
||||
changeHandler(event: MatRadioChange) {
|
||||
this.setValue(event.value);
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
|
||||
const initialValue = this.getSelectedValue();
|
||||
if (initialValue !== null) {
|
||||
this.setValue(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
const initialValue = this.getSelectedValue();
|
||||
if (initialValue !== null) {
|
||||
this.setValue(initialValue);
|
||||
this.updateDisplayValue();
|
||||
this.context.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,8 @@
|
||||
data-automation-id="slider-range">
|
||||
</mat-slider>
|
||||
|
||||
<div class="adf-facet-buttons">
|
||||
<button mat-button color="primary" (click)="reset()" data-automation-id="slider-btn-clear">
|
||||
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
|
||||
<button mat-button color="primary" (click)="clear()" data-automation-id="slider-btn-clear">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -74,14 +74,13 @@ describe('SearchSliderComponent', () => {
|
||||
component.context = context;
|
||||
component.id = 'contentSize';
|
||||
component.settings = { field: 'cm:content.size' };
|
||||
fixture.detectChanges();
|
||||
|
||||
component.onChangedHandler(<MatSliderChange> { value: 10 });
|
||||
fixture.detectChanges();
|
||||
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
|
||||
expect(context.update).toHaveBeenCalled();
|
||||
|
||||
component.onChangedHandler(<MatSliderChange> { value: 20 });
|
||||
fixture.detectChanges();
|
||||
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]');
|
||||
});
|
||||
|
||||
|
@@ -15,11 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { MatSliderChange } from '@angular/material/slider';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-slider',
|
||||
@@ -39,6 +40,8 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
||||
min: number;
|
||||
max: number;
|
||||
thumbLabel = false;
|
||||
enableChangeUpdate: boolean;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
/** The numeric value represented by the slider. */
|
||||
@Input()
|
||||
@@ -59,6 +62,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
|
||||
this.thumbLabel = this.settings['thumbLabel'] ? true : false;
|
||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||
}
|
||||
|
||||
if (this.startValue) {
|
||||
@@ -66,6 +70,13 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.value = this.min || 0;
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateQuery(null);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.value = this.min || 0;
|
||||
this.updateQuery(null);
|
||||
@@ -73,7 +84,9 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
||||
|
||||
onChangedHandler(event: MatSliderChange) {
|
||||
this.value = event.value;
|
||||
this.updateQuery(this.value);
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateQuery(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
submitValues() {
|
||||
@@ -94,6 +107,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
|
||||
private updateQuery(value: number | null) {
|
||||
this.displayValue$.next( this.value ? `${this.value} ${this.settings.unit ?? ''}` : '' );
|
||||
if (this.id && this.context && this.settings && this.settings.field) {
|
||||
if (value === null) {
|
||||
this.context.queryFragments[this.id] = '';
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { SearchSortingPickerComponent } from './search-sorting-picker.component';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { SearchConfiguration } from '../../models/search-configuration.interface';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Component, OnInit, ViewEncapsulation, Inject } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { SearchSortingDefinition } from '../../models/search-sorting-definition.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
placeholder="{{ settings?.placeholder | translate }}"
|
||||
[(ngModel)]="value"
|
||||
(change)="onChangedHandler($event)">
|
||||
<button mat-button *ngIf="value" matSuffix mat-icon-button (click)="reset()">
|
||||
<button mat-button *ngIf="value" matSuffix mat-icon-button (click)="clear()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
@@ -15,10 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { SearchWidget } from '../../models/search-widget.interface';
|
||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-text',
|
||||
@@ -39,6 +40,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
||||
startValue: string;
|
||||
isActive = false;
|
||||
enableChangeUpdate = true;
|
||||
displayValue$: Subject<string> = new Subject<string>();
|
||||
|
||||
ngOnInit() {
|
||||
if (this.context && this.settings && this.settings.pattern) {
|
||||
@@ -59,9 +61,15 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
clear() {
|
||||
this.isActive = false;
|
||||
this.value = '';
|
||||
if (this.enableChangeUpdate) {
|
||||
this.updateQuery(null);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.value = '';
|
||||
this.updateQuery(null);
|
||||
}
|
||||
@@ -75,11 +83,11 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
||||
}
|
||||
|
||||
private updateQuery(value: string) {
|
||||
this.displayValue$.next(value);
|
||||
if (this.context && this.settings && this.settings.field) {
|
||||
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
|
||||
this.context.update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
submitValues() {
|
||||
@@ -96,6 +104,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
||||
|
||||
setValue(value: string) {
|
||||
this.value = value;
|
||||
this.displayValue$.next(this.value);
|
||||
this.submitValues();
|
||||
}
|
||||
|
||||
|
@@ -15,10 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, ComponentRef, ComponentFactoryResolver, Inject, SimpleChanges, OnChanges } from '@angular/core';
|
||||
import { SearchFilterService } from '../search-filter/search-filter.service';
|
||||
import { BaseQueryBuilderService } from '../../base-query-builder.service';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ComponentRef,
|
||||
ComponentFactoryResolver,
|
||||
Inject,
|
||||
SimpleChanges,
|
||||
OnChanges
|
||||
} from '@angular/core';
|
||||
import { SearchFilterService } from '../../services/search-filter.service';
|
||||
import { BaseQueryBuilderService } from '../../services/base-query-builder.service';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-widget-container',
|
||||
@@ -74,7 +87,7 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan
|
||||
private setupWidget(ref: ComponentRef<any>) {
|
||||
if (ref && ref.instance) {
|
||||
ref.instance.id = this.id;
|
||||
ref.instance.settings = { ...this.settings };
|
||||
ref.instance.settings = {...this.settings};
|
||||
ref.instance.context = this.queryBuilder;
|
||||
if (this.value) {
|
||||
ref.instance.isActive = true;
|
||||
@@ -107,6 +120,13 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan
|
||||
return this.componentRef.instance.getCurrentValue();
|
||||
}
|
||||
|
||||
getDisplayValue(): Observable<string> | null {
|
||||
if (!this.componentRef?.instance) {
|
||||
return null;
|
||||
}
|
||||
return this.componentRef.instance.displayValue$;
|
||||
}
|
||||
|
||||
resetInnerWidget() {
|
||||
if (this.componentRef && this.componentRef.instance) {
|
||||
this.componentRef.instance.reset();
|
||||
|
@@ -31,5 +31,13 @@ export interface FacetField {
|
||||
currentPageSize?: number;
|
||||
checked?: boolean;
|
||||
type?: string;
|
||||
settings?: FacetFieldSettings;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface FacetFieldSettings {
|
||||
/* allow the user to update search in every change */
|
||||
allowUpdateOnChange?: boolean;
|
||||
/* allow the user show/hide default search actions */
|
||||
hideDefaultAction?: boolean;
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { Subject } from 'rxjs';
|
||||
|
||||
export interface FacetWidget {
|
||||
/* provide the formatted selected value for chip */
|
||||
displayValue$: Subject<string>;
|
||||
/* reset the value and update the search */
|
||||
reset(): void;
|
||||
/* update the search with field value */
|
||||
submitValues(): void;
|
||||
}
|
@@ -17,7 +17,7 @@
|
||||
|
||||
import { FilterQuery } from './filter-query.interface';
|
||||
import { FacetQuery } from './facet-query.interface';
|
||||
import { FacetField } from './facet-field.interface';
|
||||
import { FacetField, FacetFieldSettings } from './facet-field.interface';
|
||||
import { SearchCategory } from './search-category.interface';
|
||||
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
||||
import { RequestHighlight } from '@alfresco/js-api';
|
||||
@@ -35,6 +35,7 @@ export interface SearchConfiguration {
|
||||
expanded?: boolean;
|
||||
mincount?: number;
|
||||
queries: FacetQuery[];
|
||||
settings?: FacetFieldSettings;
|
||||
};
|
||||
facetFields?: {
|
||||
expanded?: boolean;
|
||||
|
@@ -17,5 +17,14 @@
|
||||
|
||||
export interface SearchWidgetSettings {
|
||||
field: string;
|
||||
/* allow the user to update search in every change */
|
||||
allowUpdateOnChange?: boolean;
|
||||
/* allow the user hide default search actions. So widget can have custom actions */
|
||||
hideDefaultAction?: boolean;
|
||||
/* describes the unit of the value i.e byte for better display message */
|
||||
unit?: string;
|
||||
/* describes query format */
|
||||
format?: string;
|
||||
|
||||
[indexer: string]: any;
|
||||
}
|
||||
|
@@ -16,17 +16,23 @@
|
||||
*/
|
||||
|
||||
import { SearchWidgetSettings } from './search-widget-settings.interface';
|
||||
import { SearchQueryBuilderService } from '../search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from '../services/search-query-builder.service';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface SearchWidget {
|
||||
id: string;
|
||||
/* optional field control options */
|
||||
settings?: SearchWidgetSettings;
|
||||
context?: SearchQueryBuilderService;
|
||||
isActive?: boolean;
|
||||
startValue: any;
|
||||
reset();
|
||||
submitValues();
|
||||
hasValidValue();
|
||||
getCurrentValue();
|
||||
/* stream emit value on changes */
|
||||
displayValue$: Subject<string>;
|
||||
/* reset the value and update the search */
|
||||
reset(): void;
|
||||
/* update the search with field value */
|
||||
submitValues(): void;
|
||||
hasValidValue(): boolean;
|
||||
getCurrentValue(): any;
|
||||
setValue(value: any);
|
||||
}
|
||||
|
@@ -24,12 +24,12 @@ export * from './models/search-category.interface';
|
||||
export * from './models/search-widget-settings.interface';
|
||||
export * from './models/search-widget.interface';
|
||||
export * from './models/search-configuration.interface';
|
||||
export * from './search-query-builder.service';
|
||||
export * from './services/search-query-builder.service';
|
||||
export * from './models/search-range.interface';
|
||||
export * from './models/search-form.interface';
|
||||
|
||||
export * from './search-query-service.token';
|
||||
export * from './search-header-query-builder.service';
|
||||
export * from './services/search-header-query-builder.service';
|
||||
|
||||
export * from './components/search.component';
|
||||
export * from './components/search-control.component';
|
||||
@@ -41,7 +41,7 @@ export * from './components/search-check-list/search-check-list.component';
|
||||
export * from './components/search-chip-list/search-chip-list.component';
|
||||
export * from './components/search-date-range/search-date-range.component';
|
||||
export * from './components/search-filter/search-filter.component';
|
||||
export * from './components/search-filter/search-filter.service';
|
||||
export * from './services/search-filter.service';
|
||||
export * from './components/search-filter-container/search-filter-container.component';
|
||||
export * from './components/search-number-range/search-number-range.component';
|
||||
export * from './components/search-radio/search-radio.component';
|
||||
@@ -52,5 +52,10 @@ export * from './components/search-text/search-text.component';
|
||||
export * from './components/search-widget-container/search-widget-container.component';
|
||||
export * from './components/search-datetime-range/search-datetime-range.component';
|
||||
export * from './components/search-form/search-form.component';
|
||||
export * from './services/search-facet-filters.service';
|
||||
export * from './components/search-filter-chips/search-filter-chips.component';
|
||||
export * from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component';
|
||||
export * from './components/search-facet-field/search-facet-field.component';
|
||||
export * from './components/reset-search.directive';
|
||||
|
||||
export * from './search.module';
|
||||
|
@@ -16,6 +16,6 @@
|
||||
*/
|
||||
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
import { BaseQueryBuilderService } from './services/base-query-builder.service';
|
||||
|
||||
export const SEARCH_QUERY_SERVICE_TOKEN = new InjectionToken<BaseQueryBuilderService>('QueryService');
|
||||
|
@@ -37,10 +37,16 @@ import { SearchCheckListComponent } from './components/search-check-list/search-
|
||||
import { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component';
|
||||
import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from './search-query-service.token';
|
||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
||||
import { SearchQueryBuilderService } from './services/search-query-builder.service';
|
||||
import { SearchFilterContainerComponent } from './components/search-filter-container/search-filter-container.component';
|
||||
import { SearchDatetimeRangeComponent } from './components/search-datetime-range/search-datetime-range.component';
|
||||
import { SearchFormComponent } from './components/search-form/search-form.component';
|
||||
import { SearchFilterChipsComponent } from './components/search-filter-chips/search-filter-chips.component';
|
||||
import { SearchFilterMenuCardComponent } from './components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component';
|
||||
import { SearchFacetFieldComponent } from './components/search-facet-field/search-facet-field.component';
|
||||
import { SearchWidgetChipComponent } from './components/search-filter-chips/search-widget-chip/search-widget-chip.component';
|
||||
import { SearchFacetChipComponent } from './components/search-filter-chips/search-facet-chip/search-facet-chip.component';
|
||||
import { ResetSearchDirective } from './components/reset-search.directive';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -67,7 +73,13 @@ import { SearchFormComponent } from './components/search-form/search-form.compon
|
||||
SearchDatetimeRangeComponent,
|
||||
SearchSortingPickerComponent,
|
||||
SearchFilterContainerComponent,
|
||||
SearchFormComponent
|
||||
SearchFormComponent,
|
||||
SearchFilterChipsComponent,
|
||||
SearchFilterMenuCardComponent,
|
||||
SearchFacetFieldComponent,
|
||||
SearchWidgetChipComponent,
|
||||
SearchFacetChipComponent,
|
||||
ResetSearchDirective
|
||||
],
|
||||
exports: [
|
||||
SearchComponent,
|
||||
@@ -86,11 +98,14 @@ import { SearchFormComponent } from './components/search-form/search-form.compon
|
||||
SearchDatetimeRangeComponent,
|
||||
SearchSortingPickerComponent,
|
||||
SearchFilterContainerComponent,
|
||||
SearchFormComponent
|
||||
SearchFormComponent,
|
||||
SearchFilterChipsComponent,
|
||||
SearchFilterMenuCardComponent,
|
||||
SearchFacetFieldComponent,
|
||||
ResetSearchDirective
|
||||
],
|
||||
providers: [
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService },
|
||||
SearchSortingPickerComponent
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService }
|
||||
]
|
||||
})
|
||||
export class SearchModule {}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject, Observable, from } from 'rxjs';
|
||||
import { Subject, Observable, from, ReplaySubject } from 'rxjs';
|
||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||
import {
|
||||
QueryBody,
|
||||
@@ -27,15 +27,15 @@ import {
|
||||
RequestHighlight,
|
||||
RequestScope
|
||||
} from '@alfresco/js-api';
|
||||
import { SearchCategory } from './models/search-category.interface';
|
||||
import { FilterQuery } from './models/filter-query.interface';
|
||||
import { SearchRange } from './models/search-range.interface';
|
||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
||||
import { FacetQuery } from './models/facet-query.interface';
|
||||
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
|
||||
import { FacetField } from './models/facet-field.interface';
|
||||
import { FacetFieldBucket } from './models/facet-field-bucket.interface';
|
||||
import { SearchForm } from './models/search-form.interface';
|
||||
import { SearchCategory } from '../models/search-category.interface';
|
||||
import { FilterQuery } from '../models/filter-query.interface';
|
||||
import { SearchRange } from '../models/search-range.interface';
|
||||
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||
import { FacetQuery } from '../models/facet-query.interface';
|
||||
import { SearchSortingDefinition } from '../models/search-sorting-definition.interface';
|
||||
import { FacetField } from '../models/facet-field.interface';
|
||||
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
|
||||
import { SearchForm } from '../models/search-form.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -54,6 +54,9 @@ export abstract class BaseQueryBuilderService {
|
||||
/* Stream that emits the error whenever user search */
|
||||
error = new Subject();
|
||||
|
||||
/* Stream that emits search forms */
|
||||
searchForms = new ReplaySubject<SearchForm[]>(1);
|
||||
|
||||
categories: SearchCategory[] = [];
|
||||
queryFragments: { [id: string]: string } = {};
|
||||
filterQueries: FilterQuery[] = [];
|
||||
@@ -92,14 +95,16 @@ export abstract class BaseQueryBuilderService {
|
||||
|
||||
public resetToDefaults() {
|
||||
const currentConfig = this.getDefaultConfiguration();
|
||||
this.resetSearchOptions();
|
||||
this.configUpdated.next(currentConfig);
|
||||
this.searchForms.next(this.getSearchFormDetails());
|
||||
this.setUpSearchConfiguration(currentConfig);
|
||||
}
|
||||
|
||||
public getDefaultConfiguration(): SearchConfiguration | undefined {
|
||||
const configurations = this.loadConfiguration();
|
||||
|
||||
if (this.selectedConfiguration >= 0) {
|
||||
if (this.selectedConfiguration !== undefined) {
|
||||
return configurations[this.selectedConfiguration];
|
||||
}
|
||||
|
||||
@@ -112,8 +117,9 @@ export abstract class BaseQueryBuilderService {
|
||||
public updateSelectedConfiguration(index: number): void {
|
||||
const currentConfig = this.loadConfiguration();
|
||||
if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) {
|
||||
this.configUpdated.next(currentConfig[index]);
|
||||
this.selectedConfiguration = index;
|
||||
this.configUpdated.next(currentConfig[index]);
|
||||
this.searchForms.next(this.getSearchFormDetails());
|
||||
this.resetSearchOptions();
|
||||
this.setUpSearchConfiguration(currentConfig[index]);
|
||||
this.update();
|
||||
@@ -126,18 +132,21 @@ export abstract class BaseQueryBuilderService {
|
||||
this.filterQueries = [];
|
||||
this.sorting = [];
|
||||
this.sortingOptions = [];
|
||||
this.userFacetBuckets = {};
|
||||
this.scope = null;
|
||||
}
|
||||
|
||||
public getSearchConfigurationDetails(): SearchForm[] {
|
||||
public getSearchFormDetails(): SearchForm[] {
|
||||
const configurations = this.loadConfiguration();
|
||||
if (Array.isArray(configurations)) {
|
||||
return configurations.map((configuration, index) => ({
|
||||
index,
|
||||
name: configuration.name || 'SEARCH.UNKNOWN_FORM',
|
||||
name: configuration.name || 'SEARCH.UNKNOWN_CONFIGURATION',
|
||||
default: configuration.default || false,
|
||||
selected: this.selectedConfiguration !== undefined ? index === this.selectedConfiguration : configuration.default
|
||||
}));
|
||||
} else if (!!configurations) {
|
||||
return [{ index: 0, name: configurations.name || 'SEARCH.UNKNOWN_CONFIGURATION', default: true, selected: true }];
|
||||
}
|
||||
return [];
|
||||
}
|
@@ -0,0 +1,419 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchFacetFiltersService } from './search-facet-filters.service';
|
||||
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
||||
|
||||
describe('SearchFacetFiltersService', () => {
|
||||
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||
let queryBuilder: SearchQueryBuilderService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ContentTestingModule]
|
||||
});
|
||||
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||
});
|
||||
|
||||
it('should subscribe to query builder executed event', () => {
|
||||
spyOn(searchFacetFiltersService, 'onDataLoaded').and.stub();
|
||||
const data = { list: {} };
|
||||
queryBuilder.executed.next(data);
|
||||
|
||||
expect(searchFacetFiltersService.onDataLoaded).toHaveBeenCalledWith(data);
|
||||
});
|
||||
|
||||
it('should fetch facet queries from response payload', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
label: 'label1',
|
||||
queries: [
|
||||
{ label: 'q1', query: 'query1' },
|
||||
{ label: 'q2', query: 'query2' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const queries = [
|
||||
{ label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: [{
|
||||
type: 'query',
|
||||
label: 'label1',
|
||||
buckets: queries
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets.length).toBe(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should preserve order after response processing', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
label: 'label1',
|
||||
queries: [
|
||||
{ label: 'q1', query: 'query1' },
|
||||
{ label: 'q2', query: 'query2' },
|
||||
{ label: 'q3', query: 'query3' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const queries = [
|
||||
{ label: 'q2', filterQuery: 'query2', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q1', filterQuery: 'query1', metrics: [{value: {count: 1}}] },
|
||||
{ label: 'q3', filterQuery: 'query3', metrics: [{value: {count: 1}}] }
|
||||
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: [{
|
||||
type: 'query',
|
||||
label: 'label1',
|
||||
buckets: queries
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets.length).toBe(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.length).toBe(3);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].label).toBe('q1');
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].label).toBe('q2');
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[2].label).toBe('q3');
|
||||
});
|
||||
|
||||
it('should not fetch facet queries from response payload', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets).toBeNull();
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1', mincount: 0 },
|
||||
{ label: 'f2', field: 'f2', mincount: 0 }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const fields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: [{ label: 'a1' }, { label: 'a2' }] },
|
||||
{ type: 'field', label: 'f2', buckets: [{ label: 'b1' }, { label: 'b2' }] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: fields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[1].buckets.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should filter response facet fields based on search filter config method', () => {
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
},
|
||||
filterWithContains: false
|
||||
};
|
||||
|
||||
const initialFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: [
|
||||
{ label: 'firstLabel', display: 'firstLabel', metrics: [{value: {count: 5}}] },
|
||||
{ label: 'secondLabel', display: 'secondLabel', metrics: [{value: {count: 5}}] },
|
||||
{ label: 'thirdLabel', display: 'thirdLabel', metrics: [{value: {count: 5}}] }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: initialFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toBe(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(3);
|
||||
|
||||
searchFacetFiltersService.responseFacets[0].buckets.filterText = 'f';
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems[0].label).toEqual('firstLabel');
|
||||
|
||||
searchFacetFiltersService.responseFacets[0].buckets.filterText = 'label';
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(0);
|
||||
|
||||
// Set filter method to use contains and test again
|
||||
queryBuilder.config.filterWithContains = true;
|
||||
searchFacetFiltersService.responseFacets[0].buckets.filterText = 'f';
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(1);
|
||||
searchFacetFiltersService.responseFacets[0].buckets.filterText = 'label';
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.visibleItems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and show the bucket values', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [
|
||||
{ label: 'b1', metrics: [{value: {count: 10}}] },
|
||||
{ label: 'b2', metrics: [{value: {count: 1}}] }
|
||||
]
|
||||
},
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets.length).toEqual(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(1);
|
||||
});
|
||||
|
||||
it('should fetch facet fields from response payload and update the existing bucket values', () => {
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [
|
||||
{ label: 'f1', field: 'f1' },
|
||||
{ label: 'f2', field: 'f2' }
|
||||
]},
|
||||
facetQueries: {
|
||||
queries: []
|
||||
}
|
||||
};
|
||||
|
||||
const initialFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets: { items: [{ label: 'b1', count: 10, filterQuery: 'filter' }, { label: 'b2', count: 1 }]} },
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
searchFacetFiltersService.responseFacets = initialFields;
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(1);
|
||||
|
||||
const serverResponseFields: any = [
|
||||
{ type: 'field', label: 'f1', buckets:
|
||||
[{ label: 'b1', metrics: [{value: {count: 6}}], filterQuery: 'filter' },
|
||||
{ label: 'b2', metrics: [{value: {count: 0}}] }] },
|
||||
{ type: 'field', label: 'f2', buckets: [] }
|
||||
];
|
||||
const data = {
|
||||
list: {
|
||||
context: {
|
||||
facets: serverResponseFields
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(6);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update correctly the existing facetFields bucket values', () => {
|
||||
searchFacetFiltersService.responseFacets = null;
|
||||
|
||||
queryBuilder.config = {
|
||||
categories: [],
|
||||
facetFields: { fields: [{ label: 'f1', field: 'f1' }] },
|
||||
facetQueries: { queries: [] }
|
||||
};
|
||||
|
||||
const firstCallFields: any = [{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [{ label: 'b1', metrics: [{value: {count: 10}}] }]
|
||||
}];
|
||||
const firstCallData = { list: { context: { facets: firstCallFields }}};
|
||||
searchFacetFiltersService.onDataLoaded(firstCallData);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(10);
|
||||
|
||||
const secondCallFields: any = [{
|
||||
type: 'field',
|
||||
label: 'f1',
|
||||
buckets: [{ label: 'b1', metrics: [{value: {count: 6}}] }]
|
||||
}];
|
||||
const secondCallData = { list: { context: { facets: secondCallFields}}};
|
||||
searchFacetFiltersService.onDataLoaded(secondCallData);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(6);
|
||||
});
|
||||
|
||||
it('should fetch facet intervals from response payload', () => {
|
||||
searchFacetFiltersService.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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets.length).toBe(2);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(2);
|
||||
expect(searchFacetFiltersService.responseFacets[1].buckets.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => {
|
||||
searchFacetFiltersService.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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchFacetFiltersService.onDataLoaded(data);
|
||||
|
||||
expect(searchFacetFiltersService.responseFacets.length).toBe(1);
|
||||
expect(searchFacetFiltersService.responseFacets[0].buckets.length).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,370 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2019 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 { Inject, Injectable, OnDestroy } from '@angular/core';
|
||||
import { FacetField } from '../models/facet-field.interface';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../search-query-service.token';
|
||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
||||
import { SearchService, TranslationService } from '@alfresco/adf-core';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { SearchFilterList } from '../models/search-filter-list.model';
|
||||
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
|
||||
|
||||
export interface SelectedBucket {
|
||||
field: FacetField;
|
||||
bucket: FacetFieldBucket;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SearchFacetFiltersService implements OnDestroy {
|
||||
|
||||
/** All facet field items to be displayed in the component. These are updated according to the response.
|
||||
* When a new search is performed, the already existing items are updated with the new bucket count values and
|
||||
* the newly received items are added to the responseFacets.
|
||||
*/
|
||||
responseFacets: FacetField[] = null;
|
||||
|
||||
/** shows the facet chips */
|
||||
selectedBuckets: SelectedBucket[] = [];
|
||||
|
||||
private DEFAULT_PAGE_SIZE = 5;
|
||||
private readonly facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
|
||||
private readonly onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
||||
private searchService: SearchService,
|
||||
private translationService: TranslationService) {
|
||||
if (queryBuilder.config && queryBuilder.config.facetQueries) {
|
||||
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
this.queryBuilder.configUpdated
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(() => {
|
||||
this.selectedBuckets = [];
|
||||
this.responseFacets = null;
|
||||
});
|
||||
|
||||
this.queryBuilder.updated
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((query) => this.queryBuilder.execute(query));
|
||||
|
||||
this.queryBuilder.executed
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((resultSetPaging: ResultSetPaging) => {
|
||||
this.onDataLoaded(resultSetPaging);
|
||||
this.searchService.dataLoaded.next(resultSetPaging);
|
||||
});
|
||||
}
|
||||
|
||||
onDataLoaded(data: any) {
|
||||
const context = data.list.context;
|
||||
|
||||
if (context) {
|
||||
this.parseFacets(context);
|
||||
} else {
|
||||
this.responseFacets = null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseFacets(context: ResultSetContext) {
|
||||
this.parseFacetFields(context);
|
||||
this.parseFacetIntervals(context);
|
||||
this.parseFacetQueries(context);
|
||||
}
|
||||
|
||||
private parseFacetItems(context: ResultSetContext, configFacetFields: FacetField[], itemType: string) {
|
||||
configFacetFields.forEach((field) => {
|
||||
const responseField = this.findFacet(context, itemType, field.label);
|
||||
const responseBuckets = this.getResponseBuckets(responseField, field)
|
||||
.filter(this.getFilterByMinCount(field.mincount));
|
||||
const alreadyExistingField = this.findResponseFacet(itemType, field.label);
|
||||
|
||||
if (alreadyExistingField) {
|
||||
const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
|
||||
|
||||
this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
|
||||
} else if (responseField) {
|
||||
if (responseBuckets.length > 0) {
|
||||
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, field.pageSize);
|
||||
bucketList.filter = this.getBucketFilterFunction(bucketList);
|
||||
|
||||
if (!this.responseFacets) {
|
||||
this.responseFacets = [];
|
||||
}
|
||||
this.responseFacets.push(<FacetField> {
|
||||
...field,
|
||||
type: responseField.type || itemType,
|
||||
label: field.label,
|
||||
pageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
|
||||
currentPageSize: field.pageSize | this.DEFAULT_PAGE_SIZE,
|
||||
buckets: bucketList
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private parseFacetFields(context: ResultSetContext) {
|
||||
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
|
||||
this.parseFacetItems(context, configFacetFields, 'field');
|
||||
}
|
||||
|
||||
private parseFacetIntervals(context: ResultSetContext) {
|
||||
const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || [];
|
||||
this.parseFacetItems(context, configFacetIntervals, 'interval');
|
||||
}
|
||||
|
||||
private parseFacetQueries(context: ResultSetContext) {
|
||||
const facetQuerySetting = this.queryBuilder.config.facetQueries?.settings || {};
|
||||
const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || [];
|
||||
const configGroups = configFacetQueries.reduce((acc, query) => {
|
||||
const group = this.queryBuilder.getQueryGroup(query);
|
||||
if (acc[group]) {
|
||||
acc[group].push(query);
|
||||
} else {
|
||||
acc[group] = [query];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount;
|
||||
const mincountFilter = this.getFilterByMinCount(mincount);
|
||||
|
||||
Object.keys(configGroups).forEach((group) => {
|
||||
const responseField = this.findFacet(context, 'query', group);
|
||||
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group])
|
||||
.filter(mincountFilter);
|
||||
const alreadyExistingField = this.findResponseFacet('query', group);
|
||||
|
||||
if (alreadyExistingField) {
|
||||
const alreadyExistingBuckets = alreadyExistingField.buckets && alreadyExistingField.buckets.items || [];
|
||||
|
||||
this.updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets);
|
||||
} else if (responseField) {
|
||||
if (responseBuckets.length > 0) {
|
||||
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, this.facetQueriesPageSize);
|
||||
bucketList.filter = this.getBucketFilterFunction(bucketList);
|
||||
|
||||
if (!this.responseFacets) {
|
||||
this.responseFacets = [];
|
||||
}
|
||||
this.responseFacets.push(<FacetField> {
|
||||
field: group,
|
||||
type: responseField.type || 'query',
|
||||
label: group,
|
||||
pageSize: this.DEFAULT_PAGE_SIZE,
|
||||
currentPageSize: this.DEFAULT_PAGE_SIZE,
|
||||
buckets: bucketList,
|
||||
settings: facetQuerySetting
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private getResponseBuckets(responseField: GenericFacetResponse, configField: FacetField): FacetFieldBucket[] {
|
||||
return ((responseField && responseField.buckets) || []).map((respBucket) => {
|
||||
|
||||
respBucket['count'] = this.getCountValue(respBucket);
|
||||
respBucket.filterQuery = respBucket.filterQuery || this.getCorrespondingFilterQuery(configField, respBucket.label);
|
||||
return <FacetFieldBucket> {
|
||||
...respBucket,
|
||||
checked: false,
|
||||
display: respBucket.display,
|
||||
label: respBucket.label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getResponseQueryBuckets(responseField: GenericFacetResponse, configGroup: any): FacetFieldBucket[] {
|
||||
return (configGroup || []).map((query) => {
|
||||
const respBucket = ((responseField && responseField.buckets) || [])
|
||||
.find((bucket) => bucket.label === query.label) || {};
|
||||
|
||||
respBucket['count'] = this.getCountValue(respBucket);
|
||||
return <FacetFieldBucket> {
|
||||
...respBucket,
|
||||
checked: false,
|
||||
display: respBucket.display,
|
||||
label: respBucket.label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getCountValue(bucket: GenericBucket): number {
|
||||
return (!!bucket && !!bucket.metrics && bucket.metrics[0]?.value?.count) || 0;
|
||||
}
|
||||
|
||||
getBucketCountDisplay(bucket: FacetFieldBucket): string {
|
||||
return bucket.count === null ? '' : `(${bucket.count})`;
|
||||
}
|
||||
|
||||
private getFilterByMinCount(mincountInput: number) {
|
||||
return (bucket) => {
|
||||
let mincount = mincountInput;
|
||||
if (mincount === undefined) {
|
||||
mincount = 1;
|
||||
}
|
||||
return bucket.count >= mincount;
|
||||
};
|
||||
}
|
||||
|
||||
private getCorrespondingFilterQuery(configFacetItem: FacetField, bucketLabel: string): string {
|
||||
let filterQuery = null;
|
||||
|
||||
if (configFacetItem.field && bucketLabel) {
|
||||
|
||||
if (configFacetItem.sets) {
|
||||
const configSet = configFacetItem.sets.find((set) => bucketLabel === set.label);
|
||||
|
||||
if (configSet) {
|
||||
filterQuery = this.buildIntervalQuery(configFacetItem.field, configSet);
|
||||
}
|
||||
|
||||
} else {
|
||||
filterQuery = `${configFacetItem.field}:"${bucketLabel}"`;
|
||||
}
|
||||
}
|
||||
|
||||
return filterQuery;
|
||||
}
|
||||
|
||||
private buildIntervalQuery(fieldName: string, interval: any): string {
|
||||
const start = interval.start;
|
||||
const end = interval.end;
|
||||
const startLimit = (interval.startInclusive === undefined || interval.startInclusive === true) ? '[' : '<';
|
||||
const endLimit = (interval.endInclusive === undefined || interval.endInclusive === true) ? ']' : '>';
|
||||
|
||||
return `${fieldName}:${startLimit}"${start}" TO "${end}"${endLimit}`;
|
||||
}
|
||||
|
||||
private findFacet(context: ResultSetContext, itemType: string, fieldLabel: string): GenericFacetResponse {
|
||||
return (context.facets || []).find((response) => response.type === itemType && response.label === fieldLabel) || {};
|
||||
}
|
||||
|
||||
private findResponseFacet(itemType: string, fieldLabel: string): FacetField {
|
||||
return (this.responseFacets || []).find((response) => response.type === itemType && response.label === fieldLabel);
|
||||
}
|
||||
|
||||
private updateExistingBuckets(responseField, responseBuckets, alreadyExistingField, alreadyExistingBuckets) {
|
||||
const bucketsToDelete = [];
|
||||
|
||||
alreadyExistingBuckets
|
||||
.map((bucket) => {
|
||||
const responseBucket = ((responseField && responseField.buckets) || []).find((respBucket) => respBucket.label === bucket.label);
|
||||
|
||||
if (!responseBucket) {
|
||||
bucketsToDelete.push(bucket);
|
||||
}
|
||||
bucket.count = this.getCountValue(responseBucket);
|
||||
return bucket;
|
||||
});
|
||||
|
||||
const hasSelection = this.selectedBuckets
|
||||
.find((selBuckets) => alreadyExistingField.label === selBuckets.field.label && alreadyExistingField.type === selBuckets.field.type);
|
||||
|
||||
if (!hasSelection && bucketsToDelete.length) {
|
||||
bucketsToDelete.forEach((bucket) => {
|
||||
alreadyExistingField.buckets.deleteItem(bucket);
|
||||
});
|
||||
}
|
||||
|
||||
responseBuckets.forEach((respBucket) => {
|
||||
const existingBucket = alreadyExistingBuckets.find((oldBucket) => oldBucket.label === respBucket.label);
|
||||
|
||||
if (!existingBucket) {
|
||||
alreadyExistingField.buckets.addItem(respBucket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getBucketFilterFunction(bucketList) {
|
||||
return (bucket: FacetFieldBucket): boolean => {
|
||||
if (bucket && bucketList.filterText) {
|
||||
const pattern = (bucketList.filterText || '').toLowerCase();
|
||||
const label = (this.translationService.instant(bucket.display) || this.translationService.instant(bucket.label)).toLowerCase();
|
||||
return this.queryBuilder.config.filterWithContains ? label.indexOf(pattern) !== -1 : label.startsWith(pattern);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
unselectFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (bucket) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
this.updateSelectedBuckets();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
||||
|
||||
/* update adf-search-chip-list component view */
|
||||
updateSelectedBuckets() {
|
||||
if (this.responseFacets) {
|
||||
this.selectedBuckets = [];
|
||||
for (const field of this.responseFacets) {
|
||||
if (field.buckets) {
|
||||
this.selectedBuckets.push(
|
||||
...this.queryBuilder.getUserFacetBuckets(field.field)
|
||||
.filter((bucket) => bucket.checked)
|
||||
.map((bucket) => {
|
||||
return {field, bucket};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selectedBuckets = [];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy$.next();
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
resetAllSelectedBuckets() {
|
||||
this.responseFacets.forEach((field) => {
|
||||
if (field && field.buckets) {
|
||||
for (const bucket of field.buckets.items) {
|
||||
bucket.checked = false;
|
||||
this.queryBuilder.removeUserFacetBucket(field, bucket);
|
||||
}
|
||||
this.updateSelectedBuckets();
|
||||
}
|
||||
});
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
|
||||
resetQueryFragments() {
|
||||
this.queryBuilder.queryFragments = {};
|
||||
this.queryBuilder.resetToDefaults();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.responseFacets = [];
|
||||
this.selectedBuckets = [];
|
||||
this.queryBuilder.resetToDefaults();
|
||||
this.queryBuilder.update();
|
||||
}
|
||||
}
|
@@ -16,13 +16,13 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
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';
|
||||
import { SearchDatetimeRangeComponent } from '../search-datetime-range/search-datetime-range.component';
|
||||
import { SearchTextComponent } from '../components/search-text/search-text.component';
|
||||
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';
|
||||
import { SearchDatetimeRangeComponent } from '../components/search-datetime-range/search-datetime-range.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
@@ -15,11 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
||||
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ContentTestingModule } from '../testing/content.testing.module';
|
||||
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||
|
||||
describe('SearchHeaderQueryBuilderService', () => {
|
||||
|
@@ -17,14 +17,14 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-core';
|
||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
||||
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
import { SearchCategory } from './models/search-category.interface';
|
||||
import { SearchCategory } from '../models/search-category.interface';
|
||||
import { MinimalNode, QueryBody } from '@alfresco/js-api';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
|
||||
import { FilterSearch } from './models/filter-search.interface';
|
||||
import { SearchSortingDefinition } from '../models/search-sorting-definition.interface';
|
||||
import { FilterSearch } from '../models/filter-search.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
@@ -16,11 +16,11 @@
|
||||
*/
|
||||
|
||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
||||
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { FacetField } from './models/facet-field.interface';
|
||||
import { FacetField } from '../models/facet-field.interface';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ContentTestingModule } from '../testing/content.testing.module';
|
||||
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||
|
||||
describe('SearchQueryBuilder', () => {
|
||||
|
||||
@@ -670,10 +670,12 @@ describe('SearchQueryBuilder', () => {
|
||||
expect(queryBody.scope).toEqual(mockScope);
|
||||
});
|
||||
|
||||
it('should return empty if array of search config not found', () => {
|
||||
const builder = new SearchQueryBuilderService(buildConfig({}), null);
|
||||
const forms = builder.getSearchConfigurationDetails();
|
||||
expect(forms).toEqual([]);
|
||||
it('should return empty if array of search config not found', (done) => {
|
||||
const builder = new SearchQueryBuilderService(buildConfig(null), null);
|
||||
builder.searchForms.subscribe((forms) => {
|
||||
expect(forms).toEqual([]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple search configuration', () => {
|
||||
@@ -728,14 +730,15 @@ describe('SearchQueryBuilder', () => {
|
||||
expect(builder.filterQueries.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should list available search form names', () => {
|
||||
const forms = builder.getSearchConfigurationDetails();
|
||||
|
||||
expect(forms).toEqual([
|
||||
{ index: 0, name: 'config1', default: true, selected: true },
|
||||
{ index: 1, name: 'config2', default: false, selected: false },
|
||||
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
|
||||
]);
|
||||
it('should list available search form names', (done) => {
|
||||
builder.searchForms.subscribe((forms) => {
|
||||
expect(forms).toEqual([
|
||||
{ index: 0, name: 'config1', default: true, selected: true },
|
||||
{ index: 1, name: 'config2', default: false, selected: false },
|
||||
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow the user switch the form', () => {
|
||||
@@ -745,15 +748,16 @@ describe('SearchQueryBuilder', () => {
|
||||
expect(builder.filterQueries.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should keep the selected configuration value', () => {
|
||||
it('should keep the selected configuration value', (done) => {
|
||||
builder.updateSelectedConfiguration(1);
|
||||
const forms = builder.getSearchConfigurationDetails();
|
||||
|
||||
expect(forms).toEqual([
|
||||
{ index: 0, name: 'config1', default: true, selected: false },
|
||||
{ index: 1, name: 'config2', default: false, selected: true },
|
||||
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
|
||||
]);
|
||||
builder.searchForms.subscribe((forms) => {
|
||||
expect(forms).toEqual([
|
||||
{ index: 0, name: 'config1', default: true, selected: false },
|
||||
{ index: 1, name: 'config2', default: false, selected: true },
|
||||
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -17,7 +17,7 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
||||
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
|
||||
@Injectable()
|
@@ -28,6 +28,10 @@
|
||||
@import '../aspect-list/aspect-list.component';
|
||||
@import '../permission-manager/components/user-icon-column/user-icon-column.component';
|
||||
@import '../permission-manager/components/user-name-column/user-name-column.component';
|
||||
@import '../search/components/search-filter-chips/search-filter-chips.component';
|
||||
@import '../search/components/search-facet-field/search-facet-field.component';
|
||||
@import '../search/components/search-form/search-form.component';
|
||||
@import '../search/components/search-filter-chips/search-filter-menu-card/search-filter-menu-card.component';
|
||||
|
||||
@mixin adf-content-services-theme($theme) {
|
||||
@include adf-breadcrumb-theme($theme);
|
||||
@@ -56,4 +60,8 @@
|
||||
@include adf-version-comparison-theme($theme);
|
||||
@include adf-content-type-dialog-theme($theme);
|
||||
@include adf-aspect-list-theme($theme);
|
||||
@include adf-search-filter-chips-theme($theme);
|
||||
@include adf-search-filter-field-theme($theme);
|
||||
@include adf-search-forms-theme($theme);
|
||||
@include adf-search-filter-menu-card($theme);
|
||||
}
|
||||
|
Reference in New Issue
Block a user