mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +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:
@@ -350,7 +350,5 @@
|
|||||||
"ACTION_TYPE": "Action Type"
|
"ACTION_TYPE": "Action Type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SEARCH_FORMS": {
|
"DEFAULT_SEARCH": "Default"
|
||||||
"ALL": "All"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,8 +366,258 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "SEARCH_FORMS.ALL",
|
"name": "DEFAULT_SEARCH",
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filterWithContains": true,
|
||||||
|
"app:fields": [
|
||||||
|
"cm:name",
|
||||||
|
"cm:title",
|
||||||
|
"cm:description",
|
||||||
|
"ia:whatEvent",
|
||||||
|
"ia:descriptionEvent",
|
||||||
|
"lnk:title",
|
||||||
|
"lnk:description",
|
||||||
|
"TEXT",
|
||||||
|
"TAG"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"path",
|
||||||
|
"allowableOperations",
|
||||||
|
"properties"
|
||||||
|
],
|
||||||
|
"sorting": {
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"label": "Name",
|
||||||
|
"type": "FIELD",
|
||||||
|
"field": "cm:name",
|
||||||
|
"ascending": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content.sizeInBytes",
|
||||||
|
"label": "Size",
|
||||||
|
"type": "FIELD",
|
||||||
|
"field": "content.size",
|
||||||
|
"ascending": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "createdByUser",
|
||||||
|
"label": "Author",
|
||||||
|
"type": "FIELD",
|
||||||
|
"field": "cm:creator",
|
||||||
|
"ascending": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "createdAt",
|
||||||
|
"label": "Created",
|
||||||
|
"type": "FIELD",
|
||||||
|
"field": "cm:created",
|
||||||
|
"ascending": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "score",
|
||||||
|
"label": "Relevance",
|
||||||
|
"type": "SCORE",
|
||||||
|
"field": "score",
|
||||||
|
"ascending": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaults": [
|
||||||
|
{
|
||||||
|
"key": "score",
|
||||||
|
"type": "FIELD",
|
||||||
|
"field": "score",
|
||||||
|
"ascending": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resetButton": true,
|
||||||
|
"filterQueries": [
|
||||||
|
{
|
||||||
|
"query": "TYPE:'cm:folder'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "NOT cm:creator:System"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"facetFields": {
|
||||||
|
"expanded": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field": "content.size",
|
||||||
|
"mincount": 1,
|
||||||
|
"label": "Folder Size",
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true,
|
||||||
|
"unit": "Bytes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "creator",
|
||||||
|
"mincount": 1,
|
||||||
|
"label": "Field created",
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "modifier",
|
||||||
|
"mincount": 1,
|
||||||
|
"label": "Folder Modifier",
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "created",
|
||||||
|
"mincount": 1,
|
||||||
|
"label": "Folder Created",
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"facetQueries": {
|
||||||
|
"label": "SEARCH.FACET_QUERIES.MY_FACET_QUERIES",
|
||||||
|
"pageSize": 5,
|
||||||
|
"expanded": true,
|
||||||
|
"mincount": 1,
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"query": "created:2019",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.CREATED_THIS_YEAR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.mimetype:text/html",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.MIMETYPE",
|
||||||
|
"group": "Type facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[0 TO 10240]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.XTRASMALL",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[10240 TO 102400]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.SMALL",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[102400 TO 1048576]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.MEDIUM",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[1048576 TO 16777216]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.LARGE",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[16777216 TO 134217728]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.XTRALARGE",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "content.size:[134217728 TO MAX]",
|
||||||
|
"label": "SEARCH.FACET_QUERIES.XXTRALARGE",
|
||||||
|
"group": "Size facet queries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"facetIntervals": {
|
||||||
|
"expanded": true,
|
||||||
|
"intervals": [
|
||||||
|
{
|
||||||
|
"label": "The Created",
|
||||||
|
"field": "cm:created",
|
||||||
|
"sets": [
|
||||||
|
{
|
||||||
|
"label": "lastYear",
|
||||||
|
"start": "2018",
|
||||||
|
"end": "2019",
|
||||||
|
"endInclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "currentYear",
|
||||||
|
"start": "NOW/YEAR",
|
||||||
|
"end": "NOW/YEAR+1YEAR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "earlier",
|
||||||
|
"start": "*",
|
||||||
|
"end": "2018",
|
||||||
|
"endInclusive": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "The Modified",
|
||||||
|
"field": "cm:modified",
|
||||||
|
"sets": [
|
||||||
|
{
|
||||||
|
"label": "2017",
|
||||||
|
"start": "2017",
|
||||||
|
"end": "2018",
|
||||||
|
"endInclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "2017-2018",
|
||||||
|
"start": "2017",
|
||||||
|
"end": "2018",
|
||||||
|
"endInclusive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "currentYear",
|
||||||
|
"start": "NOW/YEAR",
|
||||||
|
"end": "NOW/YEAR+1YEAR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "earlierThan2017",
|
||||||
|
"start": "*",
|
||||||
|
"end": "2017",
|
||||||
|
"endInclusive": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"allowUpdateOnChange": false,
|
||||||
|
"hideDefaultAction": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"categories": [],
|
||||||
|
"highlight": {
|
||||||
|
"prefix": " ",
|
||||||
|
"postfix": " ",
|
||||||
|
"mergeContiguous": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"field": "cm:title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "description",
|
||||||
|
"prefix": "(",
|
||||||
|
"postfix": ")"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "Folder"
|
||||||
}],
|
}],
|
||||||
"search-headers": {
|
"search-headers": {
|
||||||
"filterWithContains": true,
|
"filterWithContains": true,
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ import localeDa from '@angular/common/locales/da';
|
|||||||
import localeSv from '@angular/common/locales/sv';
|
import localeSv from '@angular/common/locales/sv';
|
||||||
import { setupAppNotifications } from './services/app-notifications-factory';
|
import { setupAppNotifications } from './services/app-notifications-factory';
|
||||||
import { AppNotificationsService } from './services/app-notifications.service';
|
import { AppNotificationsService } from './services/app-notifications.service';
|
||||||
|
import { SearchFilterChipsComponent } from './components/search/search-filter-chips.component';
|
||||||
|
|
||||||
registerLocaleData(localeFr);
|
registerLocaleData(localeFr);
|
||||||
registerLocaleData(localeDe);
|
registerLocaleData(localeDe);
|
||||||
@@ -204,7 +205,8 @@ registerLocaleData(localeSv);
|
|||||||
CustomEditorComponent,
|
CustomEditorComponent,
|
||||||
CustomWidgetComponent,
|
CustomWidgetComponent,
|
||||||
ProcessCloudLayoutComponent,
|
ProcessCloudLayoutComponent,
|
||||||
ServiceTaskListCloudDemoComponent
|
ServiceTaskListCloudDemoComponent,
|
||||||
|
SearchFilterChipsComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { FilteredSearchComponent } from './components/files/filtered-search.comp
|
|||||||
import { ProcessCloudLayoutComponent } from './components/cloud/process-cloud-layout.component';
|
import { ProcessCloudLayoutComponent } from './components/cloud/process-cloud-layout.component';
|
||||||
import { ServiceTaskListCloudDemoComponent } from './components/cloud/service-task-list-cloud-demo.component';
|
import { ServiceTaskListCloudDemoComponent } from './components/cloud/service-task-list-cloud-demo.component';
|
||||||
import { AspectListSampleComponent } from './components/aspect-list-sample/aspect-list-sample.component';
|
import { AspectListSampleComponent } from './components/aspect-list-sample/aspect-list-sample.component';
|
||||||
|
import { SearchFilterChipsComponent } from './components/search/search-filter-chips.component';
|
||||||
|
|
||||||
export const appRoutes: Routes = [
|
export const appRoutes: Routes = [
|
||||||
{ path: 'login', loadChildren: () => import('./components/login/login.module').then(m => m.AppLoginModule) },
|
{ path: 'login', loadChildren: () => import('./components/login/login.module').then(m => m.AppLoginModule) },
|
||||||
@@ -325,6 +326,11 @@ export const appRoutes: Routes = [
|
|||||||
component: SearchResultComponent,
|
component: SearchResultComponent,
|
||||||
canActivate: [AuthGuardEcm]
|
canActivate: [AuthGuardEcm]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'search-filter-chips',
|
||||||
|
component: SearchFilterChipsComponent,
|
||||||
|
canActivate: [AuthGuardEcm]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'extendedSearch',
|
path: 'extendedSearch',
|
||||||
component: SearchExtendedComponent,
|
component: SearchExtendedComponent,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<div class="app-search-results">
|
||||||
|
<div class="app-search-results__content">
|
||||||
|
<mat-progress-bar *ngIf="isLoading" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||||
|
<div class="app-search-results__sorting">
|
||||||
|
<div style="padding: 0 12px">
|
||||||
|
<adf-search-form></adf-search-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-divider [vertical]="true"></mat-divider>
|
||||||
|
|
||||||
|
<div style="padding: 0 12px; flex: 65%">
|
||||||
|
<adf-search-filter-chips></adf-search-filter-chips>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-icon adf-reset-search matTooltip="reset" style="padding-right: 5px;">refresh</mat-icon>
|
||||||
|
|
||||||
|
<adf-search-sorting-picker style="width: 240px"></adf-search-sorting-picker>
|
||||||
|
</div>
|
||||||
|
<app-files-component
|
||||||
|
[showHeader]="showHeader"
|
||||||
|
[sorting]="sorting"
|
||||||
|
[sortingMode]="'server'"
|
||||||
|
[showRecentFiles]="false"
|
||||||
|
[showSitePicker]="false"
|
||||||
|
[showSettingsPanel]="false"
|
||||||
|
[currentFolderId]="null"
|
||||||
|
[nodeResult]="$any(data)"
|
||||||
|
[disableDragArea]="true"
|
||||||
|
[pagination]="pagination"
|
||||||
|
[searchTerm]="searchedWord"
|
||||||
|
(changedPageSize)="onRefreshPagination($event)"
|
||||||
|
(changedPageNumber)="onRefreshPagination($event)"
|
||||||
|
(turnedNextPage)="onRefreshPagination($event)"
|
||||||
|
(loadNext)="onRefreshPagination($event)"
|
||||||
|
(turnedPreviousPage)="onRefreshPagination($event)"
|
||||||
|
(deleteElementSuccess)="onDeleteElementSuccess()">
|
||||||
|
</app-files-component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.app-search-results {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
.app-search-settings {
|
||||||
|
width: 260px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__facets {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sorting {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.app-search-results-container {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-search-title {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
:host .app-col-display-name {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
:host .app-col-modified-at, :host .app-col-modified-by {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:host div.app-search-results-container table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*!
|
||||||
|
* @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, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
|
import { Pagination, ResultSetPaging } from '@alfresco/js-api';
|
||||||
|
import { SearchForm, SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||||
|
import { SearchService, ShowHeaderMode, UserPreferencesService } from '@alfresco/adf-core';
|
||||||
|
import { combineLatest, Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-filter-chips',
|
||||||
|
templateUrl: './search-filter-chips.component.html',
|
||||||
|
styleUrls: [ './search-filter-chips.component.scss' ],
|
||||||
|
providers: [SearchService]
|
||||||
|
})
|
||||||
|
export class SearchFilterChipsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
queryParamName = 'q';
|
||||||
|
searchedWord = '';
|
||||||
|
data: ResultSetPaging;
|
||||||
|
pagination: Pagination;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
sorting = ['name', 'asc'];
|
||||||
|
searchForms: SearchForm[];
|
||||||
|
showHeader = ShowHeaderMode.Always;
|
||||||
|
|
||||||
|
private onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
|
constructor(public router: Router,
|
||||||
|
private preferences: UserPreferencesService,
|
||||||
|
private queryBuilder: SearchQueryBuilderService,
|
||||||
|
private route: ActivatedRoute) {
|
||||||
|
combineLatest([this.route.params, this.queryBuilder.configUpdated])
|
||||||
|
.pipe(takeUntil(this.onDestroy$))
|
||||||
|
.subscribe(([params, searchConfig]) => {
|
||||||
|
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||||
|
const query = this.formatSearchQuery(this.searchedWord, searchConfig['app:fields']);
|
||||||
|
if (query) {
|
||||||
|
this.queryBuilder.userQuery = query;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
queryBuilder.paging = {
|
||||||
|
maxItems: this.preferences.paginationSize,
|
||||||
|
skipCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.queryBuilder.resetToDefaults();
|
||||||
|
|
||||||
|
this.sorting = this.getSorting();
|
||||||
|
|
||||||
|
this.queryBuilder.updated
|
||||||
|
.pipe(takeUntil(this.onDestroy$))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.sorting = this.getSorting();
|
||||||
|
this.isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queryBuilder.executed
|
||||||
|
.pipe(takeUntil(this.onDestroy$))
|
||||||
|
.subscribe((resultSetPaging: ResultSetPaging) => {
|
||||||
|
this.queryBuilder.paging.skipCount = 0;
|
||||||
|
|
||||||
|
this.onSearchResultLoaded(resultSetPaging);
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.route) {
|
||||||
|
this.route.params.forEach((params: Params) => {
|
||||||
|
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||||
|
if (this.searchedWord) {
|
||||||
|
this.queryBuilder.update();
|
||||||
|
} else {
|
||||||
|
this.queryBuilder.userQuery = null;
|
||||||
|
this.queryBuilder.executed.next(new ResultSetPaging({
|
||||||
|
list: {
|
||||||
|
pagination: { totalItems: 0 },
|
||||||
|
entries: []
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatSearchQuery(userInput: string, fields = ['cm:name']) {
|
||||||
|
if (!userInput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return fields.map((field) => `${field}:"${userInput}*"`).join(' OR ');
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.onDestroy$.next(true);
|
||||||
|
this.onDestroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchResultLoaded(resultSetPaging: ResultSetPaging) {
|
||||||
|
this.data = resultSetPaging;
|
||||||
|
this.pagination = { ...resultSetPaging.list.pagination };
|
||||||
|
}
|
||||||
|
|
||||||
|
onRefreshPagination(pagination: Pagination) {
|
||||||
|
this.queryBuilder.paging = {
|
||||||
|
maxItems: pagination.maxItems,
|
||||||
|
skipCount: pagination.skipCount
|
||||||
|
};
|
||||||
|
this.queryBuilder.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteElementSuccess() {
|
||||||
|
this.queryBuilder.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSorting(): string[] {
|
||||||
|
const primary = this.queryBuilder.getPrimarySorting();
|
||||||
|
|
||||||
|
if (primary) {
|
||||||
|
return [primary.key, primary.ascending ? 'asc' : 'desc'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['name', 'asc'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
<div class="app-search-results__sorting">
|
<div class="app-search-results__sorting">
|
||||||
<adf-search-sorting-picker></adf-search-sorting-picker>
|
<adf-search-sorting-picker></adf-search-sorting-picker>
|
||||||
|
|
||||||
<adf-search-form (formChange)="onFormChange($event)"></adf-search-form>
|
<div style="padding: 0 12px">
|
||||||
|
<button mat-stroked-button (click)="switchLayout()"> Switch to new layout </button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-files-component
|
<app-files-component
|
||||||
[showHeader]="showHeader"
|
[showHeader]="showHeader"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__sorting {
|
&__sorting {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export class SearchResultComponent implements OnInit, OnDestroy {
|
|||||||
return ['name', 'asc'];
|
return ['name', 'asc'];
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormChange(form: SearchForm) {
|
switchLayout() {
|
||||||
this.queryBuilder.updateSelectedConfiguration(form.index);
|
this.router.navigate(['search-filter-chips', { q: this.searchedWord }] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Implements a checklist [widget](../../../lib/testing/src/lib/core/pages/form/wid
|
|||||||
| operator | `string` | Logical operator to combine query fragments. Can be 'AND' or 'OR'. |
|
| operator | `string` | Logical operator to combine query fragments. Can be 'AND' or 'OR'. |
|
||||||
| options | `array` | Array of objects with `name` and `value` properties. Each object defines a checkbox, labelled with `name`, that adds the query fragment in `value` to the query when enabled. |
|
| options | `array` | Array of objects with `name` and `value` properties. Each object defines a checkbox, labelled with `name`, that adds the query fragment in `value` to the query when enabled. |
|
||||||
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true.
|
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true.
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/search
|
|||||||
| field | string | Field to apply the query to. Required value |
|
| field | string | Field to apply the query to. Required value |
|
||||||
| dateFormat | string | Date format. Dates used by the datepicker are [Moment.js](https://momentjs.com/docs/#/parsing/string-format/) instances, so you can use any date format supported by Moment. Default is 'DD/MM/YYYY'. |
|
| dateFormat | string | Date format. Dates used by the datepicker are [Moment.js](https://momentjs.com/docs/#/parsing/string-format/) instances, so you can use any date format supported by Moment. Default is 'DD/MM/YYYY'. |
|
||||||
| maxDate | string | A fixed date or the string `"today"` that will set the maximum searchable date. Default is no maximum. |
|
| maxDate | string | A fixed date or the string `"today"` that will set the maximum searchable date. Default is no maximum. |
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Implements a [search widget](../../../lib/content-services/src/lib/search/search
|
|||||||
| field | string | Field to apply the query to. Required value |
|
| field | string | Field to apply the query to. Required value |
|
||||||
| datetimeFormat | string | Datetime format. Datetime formats used by the datetime picker are [Moment.js](https://momentjs.com/docs/#/parsing/string-format/) instances, so you can use any datetime format supported by Moment. Default is 'DD/MM/YYYY HH:mm'. |
|
| datetimeFormat | string | Datetime format. Datetime formats used by the datetime picker are [Moment.js](https://momentjs.com/docs/#/parsing/string-format/) instances, so you can use any datetime format supported by Moment. Default is 'DD/MM/YYYY HH:mm'. |
|
||||||
| maxDatetime | string | A fixed datetime that will set the maximum searchable datetime. Default is no maximum. |
|
| maxDatetime | string | A fixed datetime that will set the maximum searchable datetime. Default is no maximum. |
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
Title: Search Filter Chips component
|
||||||
|
Added: v4.5.0
|
||||||
|
Status: Active
|
||||||
|
Last reviewed: 2021-06-26
|
||||||
|
---
|
||||||
|
|
||||||
|
# [Search Filter chip component](../../../lib/content-services/src/lib/search/components/search-filter-chips/search-filter-chips.component.ts "Defined in search-filter-chips.component.ts")
|
||||||
|
|
||||||
|
Represents a chip based container component for custom search and faceted search settings.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Basic usage](#basic-usage)
|
||||||
|
- [Properties](#properties)
|
||||||
|
- [Details](#details)
|
||||||
|
- [See also](#see-also)
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```html
|
||||||
|
<adf-search-filter-chips [showContextFacets]=true></adf-search-filter-chips>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Default value | Description |
|
||||||
|
| ---- | ---- | ------------- | ----------- |
|
||||||
|
| showContextFacets | `boolean` | true | Toggles whether to show or not the context facet filters |
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
This component is chip based layout for searching. it just alternate component for [expanded panel search filter](./search-filter.component.md)
|
||||||
|
|
||||||
|
You may find it useful to check out the following resources for background information
|
||||||
|
before customizing the search UI:
|
||||||
|
|
||||||
|
- [Search API](https://docs.alfresco.com/5.2/concepts/search-api.html)
|
||||||
|
- [Alfresco Full Text Search Reference](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-intro.html)
|
||||||
|
- [ACS API Explorer](https://api-explorer.alfresco.com/api-explorer/#!/search/search)
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Search Filter Component](./search-filter.component.md)
|
||||||
|
- [Search Query Builder service](../services/search-query-builder.service.md)
|
||||||
|
- [Search Widget Interface](../interfaces/search-widget.interface.md)
|
||||||
|
- [Search check list component](search-check-list.component.md)
|
||||||
|
- [Search date range component](search-date-range.component.md)
|
||||||
|
- [Search number range component](search-number-range.component.md)
|
||||||
|
- [Search radio component](search-radio.component.md)
|
||||||
|
- [Search slider component](search-slider.component.md)
|
||||||
|
- [Search text component](search-text.component.md)
|
||||||
@@ -40,6 +40,7 @@ Implements a number range [widget](../../../lib/testing/src/lib/core/pages/form/
|
|||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| field | string | Field to to use |
|
| field | string | Field to to use |
|
||||||
| format | string | Value format. Uses string substitution to allow all sorts of [range queries](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-ranges.html). |
|
| format | string | Value format. Uses string substitution to allow all sorts of [range queries](https://docs.alfresco.com/5.2/concepts/rm-searchsyntax-ranges.html). |
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Implements a radio button list [widget](../../../lib/testing/src/lib/core/pages/
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| options | `array` | Array of objects with `name` and `value` properties. Each object defines a radio button, labelled with `name`, that adds the query fragment in `value` to the query when enabled. |
|
| options | `array` | Array of objects with `name` and `value` properties. Each object defines a radio button, labelled with `name`, that adds the query fragment in `value` to the query when enabled. |
|
||||||
|
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true.
|
||||||
|
| hideDefaultAction | `boolean` | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ Implements a numeric slider [widget](../../../lib/testing/src/lib/core/pages/for
|
|||||||
| max | number | Maximum numeric value at the right end of the slider |
|
| max | number | Maximum numeric value at the right end of the slider |
|
||||||
| step | number | The step between adjacent positions on the slider |
|
| step | number | The step between adjacent positions on the slider |
|
||||||
| thumbLabel | boolean | Toggles whether the "thumb" of the slider should show the current value |
|
| thumbLabel | boolean | Toggles whether the "thumb" of the slider should show the current value |
|
||||||
|
| allowUpdateOnChange | boolean | Enable/Disable the update fire event when text has been changed. By default is true.
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Implements a text input [widget](../../../lib/testing/src/lib/core/pages/form/wi
|
|||||||
| searchSuffix | string | Text to append always in the search of a string|
|
| searchSuffix | string | Text to append always in the search of a string|
|
||||||
| searchPrefix | string | Text to prepend always in the search of a string|
|
| searchPrefix | string | Text to prepend always in the search of a string|
|
||||||
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true.
|
| allowUpdateOnChange | `boolean` | Enable/Disable the update fire event when text has been changed. By default is true.
|
||||||
|
| hideDefaultAction | boolean | Show/hide the widget actions. By default is false.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
+1
-1
@@ -50,7 +50,7 @@ import { CustomResourcesService } from '../document-list/services/custom-resourc
|
|||||||
import { NodeEntryEvent, ShareDataRow } from '../document-list';
|
import { NodeEntryEvent, ShareDataRow } from '../document-list';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../search/search-query-service.token';
|
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';
|
import { ContentNodeSelectorPanelService } from './content-node-selector-panel.service';
|
||||||
|
|
||||||
export type ValidationFunction = (entry: Node) => boolean;
|
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 { DocumentListModule } from '../document-list/document-list.module';
|
||||||
import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component';
|
import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component';
|
||||||
import { UploadModule } from '../upload/upload.module';
|
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';
|
import { ContentDirectiveModule } from '../directives/content-directive.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { SearchService, setupTestBed, DataTableComponent, DataSorting } from '@alfresco/adf-core';
|
import { SearchService, setupTestBed, DataTableComponent, DataSorting } from '@alfresco/adf-core';
|
||||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||||
import { SimpleChange } from '@angular/core';
|
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 { SEARCH_QUERY_SERVICE_TOKEN } from './../../../search/search-query-service.token';
|
||||||
import { DocumentListComponent } from './../document-list.component';
|
import { DocumentListComponent } from './../document-list.component';
|
||||||
import { FilterHeaderComponent } from './filter-header.component';
|
import { FilterHeaderComponent } from './filter-header.component';
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import { Component, Inject, OnInit, OnChanges, SimpleChanges, Input, Output, Eve
|
|||||||
import { PaginationModel, DataSorting } from '@alfresco/adf-core';
|
import { PaginationModel, DataSorting } from '@alfresco/adf-core';
|
||||||
import { DocumentListComponent } from '../document-list.component';
|
import { DocumentListComponent } from '../document-list.component';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search/search-query-service.token';
|
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 { FilterSearch } from './../../../search/models/filter-search.interface';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|||||||
@@ -223,14 +223,17 @@
|
|||||||
},
|
},
|
||||||
"FILTER": {
|
"FILTER": {
|
||||||
"ACTIONS": {
|
"ACTIONS": {
|
||||||
|
"SEARCH": "Search",
|
||||||
"CLEAR": "Clear",
|
"CLEAR": "Clear",
|
||||||
"APPLY": "Apply",
|
"APPLY": "Apply",
|
||||||
"CLEAR-ALL": "Clear all",
|
"CLEAR-ALL": "Clear all",
|
||||||
"SHOW-MORE": "Show more",
|
"SHOW-MORE": "Show more",
|
||||||
"SHOW-LESS": "Show less",
|
"SHOW-LESS": "Show less"
|
||||||
"FILTER-CATEGORY": "Filter category"
|
|
||||||
},
|
},
|
||||||
"BUTTONS": {
|
"BUTTONS": {
|
||||||
|
"CLOSE": "Close",
|
||||||
|
"REMOVE": "Remove",
|
||||||
|
"APPLY": "Apply",
|
||||||
"CLEAR-ALL": {
|
"CLEAR-ALL": {
|
||||||
"LABEL": "Clear all",
|
"LABEL": "Clear all",
|
||||||
"TOOLTIP": "This will remove all selections"
|
"TOOLTIP": "This will remove all selections"
|
||||||
@@ -290,8 +293,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FORMS": "Search Forms",
|
"UNKNOWN_CONFIGURATION": "Unknown Configuration",
|
||||||
"UNKNOWN_FORM": "Unknown Configuration",
|
|
||||||
"SEARCH_HEADER" : {
|
"SEARCH_HEADER" : {
|
||||||
"TITLE":"Filter",
|
"TITLE":"Filter",
|
||||||
"TYPE": "Type",
|
"TYPE": "Type",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { SearchCategory } from '../search';
|
||||||
|
|
||||||
export const expandableCategories = [
|
export const expandableCategories = [
|
||||||
{
|
{
|
||||||
id: 'cat-1',
|
id: 'cat-1',
|
||||||
@@ -68,7 +70,7 @@ export const expandedCategories = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const simpleCategories = [
|
export const simpleCategories: SearchCategory[] = [
|
||||||
{
|
{
|
||||||
id: 'queryName',
|
id: 'queryName',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -76,7 +78,9 @@ export const simpleCategories = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
component: {
|
component: {
|
||||||
selector: 'text',
|
selector: 'text',
|
||||||
settings: {}
|
settings: {
|
||||||
|
field: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,7 +91,7 @@ export const simpleCategories = [
|
|||||||
component: {
|
component: {
|
||||||
selector: 'check-list',
|
selector: 'check-list',
|
||||||
settings: {
|
settings: {
|
||||||
'field': null,
|
'field': 'check-list',
|
||||||
'pageSize': 5,
|
'pageSize': 5,
|
||||||
'options': [
|
'options': [
|
||||||
{ 'name': 'Folder', 'value': "TYPE:'cm:folder'" },
|
{ 'name': 'Folder', 'value': "TYPE:'cm:folder'" },
|
||||||
@@ -624,6 +628,7 @@ export const mockContentSizeResponseBucket = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getMockSearchResultWithResponseBucket() {
|
export function getMockSearchResultWithResponseBucket() {
|
||||||
mockSearchResult.list.context.facets[3].buckets.push(mockContentSizeResponseBucket);
|
const cloneResult = JSON.parse(JSON.stringify( mockSearchResult));
|
||||||
return 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) { }
|
||||||
|
}
|
||||||
+4
-5
@@ -6,8 +6,7 @@
|
|||||||
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
[attr.data-automation-id]="'checkbox-' + (option.name)"
|
||||||
(change)="changeHandler($event, option)"
|
(change)="changeHandler($event, option)"
|
||||||
class="adf-facet-filter">
|
class="adf-facet-filter">
|
||||||
<div
|
<div matTooltip="{{ option.name | translate }}"
|
||||||
matTooltip="{{ option.name | translate }}"
|
|
||||||
matTooltipPosition="right"
|
matTooltipPosition="right"
|
||||||
class="facet-name">
|
class="facet-name">
|
||||||
{{ option.name | translate }}
|
{{ option.name | translate }}
|
||||||
@@ -16,8 +15,8 @@
|
|||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="adf-facet-buttons" *ngIf="options.fitsPage">
|
<div class="adf-facet-buttons" *ngIf="options.fitsPage && !settings?.hideDefaultAction">
|
||||||
<button mat-button color="primary" (click)="reset()">
|
<button mat-button color="primary" (click)="clear()">
|
||||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +24,7 @@
|
|||||||
<div class="adf-facet-buttons" *ngIf="!options.fitsPage">
|
<div class="adf-facet-buttons" *ngIf="!options.fitsPage">
|
||||||
<button mat-icon-button
|
<button mat-icon-button
|
||||||
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
|
title="{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}"
|
||||||
(click)="reset()">
|
(click)="clear()">
|
||||||
<mat-icon>clear</mat-icon>
|
<mat-icon>clear</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button
|
<button mat-icon-button
|
||||||
|
|||||||
+33
-9
@@ -15,12 +15,14 @@
|
|||||||
* limitations under the License.
|
* 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 { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { SearchFilterList } from '../../models/search-filter-list.model';
|
||||||
|
import { TranslationService } from '@alfresco/adf-core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
export interface SearchListOption {
|
export interface SearchListOption {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -46,8 +48,9 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
|||||||
pageSize = 5;
|
pageSize = 5;
|
||||||
isActive = false;
|
isActive = false;
|
||||||
enableChangeUpdate = true;
|
enableChangeUpdate = true;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor(private translationService: TranslationService) {
|
||||||
this.options = new SearchFilterList<SearchListOption>();
|
this.options = new SearchFilterList<SearchListOption>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +62,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
|||||||
if (this.settings.options && this.settings.options.length > 0) {
|
if (this.settings.options && this.settings.options.length > 0) {
|
||||||
this.options = new SearchFilterList(this.settings.options, this.pageSize);
|
this.options = new SearchFilterList(this.settings.options, this.pageSize);
|
||||||
}
|
}
|
||||||
|
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||||
if (this.settings.allowUpdateOnChange !== undefined &&
|
|
||||||
this.settings.allowUpdateOnChange !== null) {
|
|
||||||
this.enableChangeUpdate = this.settings.allowUpdateOnChange;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.startValue) {
|
if (this.startValue) {
|
||||||
@@ -71,18 +70,42 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
clear() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
this.clearOptions();
|
||||||
|
if (this.id && this.context && this.enableChangeUpdate) {
|
||||||
|
this.updateDisplayValue();
|
||||||
|
this.context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearOptions() {
|
||||||
this.options.items.forEach((opt) => {
|
this.options.items.forEach((opt) => {
|
||||||
opt.checked = false;
|
opt.checked = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.id && this.context) {
|
if (this.id && this.context) {
|
||||||
this.context.queryFragments[this.id] = '';
|
this.context.queryFragments[this.id] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.isActive = false;
|
||||||
|
this.clearOptions();
|
||||||
|
if (this.id && this.context) {
|
||||||
|
this.updateDisplayValue();
|
||||||
this.context.update();
|
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) {
|
changeHandler(event: MatCheckboxChange, option: any) {
|
||||||
option.checked = event.checked;
|
option.checked = event.checked;
|
||||||
const checkedValues = this.getCheckedValues();
|
const checkedValues = this.getCheckedValues();
|
||||||
@@ -118,6 +141,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
|
|||||||
const query = checkedValues.join(` ${this.operator} `);
|
const query = checkedValues.join(` ${this.operator} `);
|
||||||
if (this.id && this.context) {
|
if (this.id && this.context) {
|
||||||
this.context.queryFragments[this.id] = query;
|
this.context.queryFragments[this.id] = query;
|
||||||
|
this.updateDisplayValue();
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -1,20 +1,20 @@
|
|||||||
<mat-chip-list>
|
<mat-chip-list>
|
||||||
<ng-container *ngIf="searchFilter && searchFilter.selectedBuckets.length">
|
<ng-container *ngIf="facetFiltersService.selectedBuckets.length">
|
||||||
<mat-chip *ngIf="clearAll && searchFilter.selectedBuckets.length > 1"
|
<mat-chip *ngIf="clearAll && facetFiltersService.selectedBuckets.length > 1"
|
||||||
data-automation-id="reset-filter"
|
data-automation-id="reset-filter"
|
||||||
color="primary"
|
color="primary"
|
||||||
selected
|
selected
|
||||||
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.TOOLTIP' | translate }}"
|
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.TOOLTIP' | translate }}"
|
||||||
matTooltipPosition="right"
|
matTooltipPosition="right"
|
||||||
(click)="searchFilter.resetAllSelectedBuckets()">
|
(click)="facetFiltersService.resetAllSelectedBuckets()">
|
||||||
{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.LABEL' | translate }}
|
{{ 'SEARCH.FILTER.BUTTONS.CLEAR-ALL.LABEL' | translate }}
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
|
|
||||||
<mat-chip
|
<mat-chip
|
||||||
data-automation-id="chip-list-entry"
|
data-automation-id="chip-list-entry"
|
||||||
*ngFor="let selection of searchFilter.selectedBuckets"
|
*ngFor="let selection of facetFiltersService.selectedBuckets"
|
||||||
[removable]="true"
|
[removable]="true"
|
||||||
(removed)="searchFilter.unselectFacetBucket(selection.field, selection.bucket)">
|
(removed)="facetFiltersService.unselectFacetBucket(selection.field, selection.bucket)">
|
||||||
{{ (selection.bucket.display || selection.bucket.label) | translate }}
|
{{ (selection.bucket.display || selection.bucket.label) | translate }}
|
||||||
<mat-icon matChipRemove>cancel</mat-icon>
|
<mat-icon matChipRemove>cancel</mat-icon>
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
|
|||||||
+15
-15
@@ -16,10 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { setupTestBed } from '@alfresco/adf-core';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
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 { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -43,8 +42,10 @@ class TestComponent {
|
|||||||
describe('SearchChipListComponent', () => {
|
describe('SearchChipListComponent', () => {
|
||||||
let fixture: ComponentFixture<TestComponent>;
|
let fixture: ComponentFixture<TestComponent>;
|
||||||
let component: TestComponent;
|
let component: TestComponent;
|
||||||
|
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||||
|
|
||||||
setupTestBed({
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
ContentTestingModule
|
ContentTestingModule
|
||||||
@@ -53,10 +54,9 @@ describe('SearchChipListComponent', () => {
|
|||||||
TestComponent
|
TestComponent
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(TestComponent);
|
fixture = TestBed.createComponent(TestComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display clear button only when entries present', () => {
|
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"]`));
|
let clearButton = fixture.debugElement.query(By.css(`[data-automation-id="reset-filter"]`));
|
||||||
expect(clearButton).toBeNull();
|
expect(clearButton).toBeNull();
|
||||||
|
|
||||||
component.searchFilter.selectedBuckets = [{
|
searchFacetFiltersService.selectedBuckets = [{
|
||||||
bucket: {
|
bucket: {
|
||||||
count: 1,
|
count: 1,
|
||||||
label: 'test',
|
label: 'test',
|
||||||
@@ -79,7 +79,7 @@ describe('SearchChipListComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reflect changes in the search filter', () => {
|
it('should reflect changes in the search filter', () => {
|
||||||
const selectedBuckets = component.searchFilter.selectedBuckets;
|
const selectedBuckets = searchFacetFiltersService.selectedBuckets;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
let chips = fixture.debugElement.queryAll(By.css(`[data-automation-id="chip-list-entry"]`));
|
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 () => {
|
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: {
|
bucket: {
|
||||||
count: 1,
|
count: 1,
|
||||||
@@ -119,15 +119,15 @@ describe('SearchChipListComponent', () => {
|
|||||||
chips[0].nativeElement.click();
|
chips[0].nativeElement.click();
|
||||||
|
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(component.searchFilter.unselectFacetBucket).toHaveBeenCalled();
|
expect(searchFacetFiltersService.unselectFacetBucket).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove items from the search filter on clear button click', () => {
|
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 selectedBucket1: any = { field: { id: 1 }, bucket: {label: 'bucket1'} };
|
||||||
const selectedBucket2: any = { field: { id: 2 }, bucket: {label: 'bucket2'} };
|
const selectedBucket2: any = { field: { id: 2 }, bucket: {label: 'bucket2'} };
|
||||||
component.searchFilter.selectedBuckets = [selectedBucket1, selectedBucket2];
|
searchFacetFiltersService.selectedBuckets = [selectedBucket1, selectedBucket2];
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -137,14 +137,14 @@ describe('SearchChipListComponent', () => {
|
|||||||
closeButtons[0].click();
|
closeButtons[0].click();
|
||||||
fixture.detectChanges();
|
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', () => {
|
it('should disable clear mode via input properties', () => {
|
||||||
spyOn(component.searchFilter, 'unselectFacetBucket').and.callThrough();
|
spyOn(component.searchFilter, 'unselectFacetBucket').and.callThrough();
|
||||||
|
|
||||||
component.allowClear = false;
|
component.allowClear = false;
|
||||||
component.searchFilter.selectedBuckets = [
|
searchFacetFiltersService.selectedBuckets = [
|
||||||
{
|
{
|
||||||
bucket: {
|
bucket: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
|||||||
+4
-1
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import { Component, ViewEncapsulation, Input } from '@angular/core';
|
import { Component, ViewEncapsulation, Input } from '@angular/core';
|
||||||
import { SearchFilterComponent } from '../../components/search-filter/search-filter.component';
|
import { SearchFilterComponent } from '../../components/search-filter/search-filter.component';
|
||||||
|
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-chip-list',
|
selector: 'adf-search-chip-list',
|
||||||
@@ -26,11 +27,13 @@ import { SearchFilterComponent } from '../../components/search-filter/search-fil
|
|||||||
})
|
})
|
||||||
export class SearchChipListComponent {
|
export class SearchChipListComponent {
|
||||||
|
|
||||||
/** Search filter to supply the data for the chips. */
|
|
||||||
@Input()
|
@Input()
|
||||||
|
/** @deprecated This is not required since ADF 4.5.0 */
|
||||||
searchFilter: SearchFilterComponent;
|
searchFilter: SearchFilterComponent;
|
||||||
|
|
||||||
/** Flag used to enable the display of a clear-all-filters button. */
|
/** Flag used to enable the display of a clear-all-filters button. */
|
||||||
@Input()
|
@Input()
|
||||||
clearAll: boolean = false;
|
clearAll: boolean = false;
|
||||||
|
|
||||||
|
constructor(public facetFiltersService: SearchFacetFiltersService) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace">
|
<div class="adf-facet-buttons adf-facet-buttons--topSpace" *ngIf="!settings?.hideDefaultAction">
|
||||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="date-range-clear-btn">
|
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="date-range-clear-btn">
|
||||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="date-range-apply-btn">
|
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="date-range-apply-btn">
|
||||||
|
|||||||
+39
-7
@@ -15,14 +15,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OnInit, Component, ViewEncapsulation, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
|
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 { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||||
import { Moment } from 'moment';
|
import { Moment } from 'moment';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@@ -64,6 +69,8 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
|||||||
fromMaxDate: any;
|
fromMaxDate: any;
|
||||||
isActive = false;
|
isActive = false;
|
||||||
startValue: any;
|
startValue: any;
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
private onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
@@ -126,6 +133,7 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.setFromMaxDate();
|
this.setFromMaxDate();
|
||||||
|
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -141,6 +149,8 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
|||||||
const end = moment(model.to).endOf('day').format();
|
const end = moment(model.to).endOf('day').format();
|
||||||
|
|
||||||
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
||||||
|
|
||||||
|
this.updateDisplayValue();
|
||||||
this.context.update();
|
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) {
|
setValue(parsedDate: string) {
|
||||||
const splitValue = parsedDate.split('||');
|
const splitValue = parsedDate.split('||');
|
||||||
const fromValue = this.dateAdapter.parse(splitValue[0], this.datePickerFormat);
|
const fromValue = this.dateAdapter.parse(splitValue[0], this.datePickerFormat);
|
||||||
@@ -172,20 +190,34 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
|||||||
this.to.markAsTouched();
|
this.to.markAsTouched();
|
||||||
this.submitValues();
|
this.submitValues();
|
||||||
}
|
}
|
||||||
|
clear() {
|
||||||
reset() {
|
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
from: '',
|
from: '',
|
||||||
to: ''
|
to: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.id && this.context) {
|
if (this.id && this.context) {
|
||||||
this.context.queryFragments[this.id] = '';
|
this.context.queryFragments[this.id] = '';
|
||||||
this.context.update();
|
if (this.enableChangeUpdate) {
|
||||||
|
this.updateQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.setFromMaxDate();
|
this.setFromMaxDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.clear();
|
||||||
|
this.updateQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateQuery() {
|
||||||
|
if (this.id && this.context) {
|
||||||
|
this.updateDisplayValue();
|
||||||
|
this.context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChangedHandler(event: any, formControl: FormControl) {
|
onChangedHandler(event: any, formControl: FormControl) {
|
||||||
|
|
||||||
const inputValue = event.value;
|
const inputValue = event.value;
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div class="adf-facet-buttons adf-facet-buttons--topSpace">
|
<div class="adf-facet-buttons adf-facet-buttons--topSpace" *ngIf="!settings?.hideDefaultAction">
|
||||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="datetime-range-clear-btn">
|
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="datetime-range-clear-btn">
|
||||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="datetime-range-apply-btn">
|
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="datetime-range-apply-btn">
|
||||||
|
|||||||
+33
-6
@@ -15,13 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OnInit, Component, ViewEncapsulation, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
|
import { UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
|
||||||
|
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||||
import { Moment } from 'moment';
|
import { Moment } from 'moment';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@@ -64,6 +64,8 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
fromMaxDatetime: any;
|
fromMaxDatetime: any;
|
||||||
isActive = false;
|
isActive = false;
|
||||||
startValue: any;
|
startValue: any;
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
private onDestroy$ = new Subject<boolean>();
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.setFromMaxDatetime();
|
this.setFromMaxDatetime();
|
||||||
|
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -134,6 +137,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
const end = moment.utc(model.to).endOf('minute').format();
|
const end = moment.utc(model.to).endOf('minute').format();
|
||||||
|
|
||||||
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
this.context.queryFragments[this.id] = `${this.settings.field}:['${start}' TO '${end}']`;
|
||||||
|
this.updateDisplayValue();
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,10 +153,18 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
getCurrentValue(): DatetimeRangeValue {
|
getCurrentValue(): DatetimeRangeValue {
|
||||||
return {
|
return {
|
||||||
from: this.dateAdapter.format(this.form.value.from, this.datetimePickerFormat),
|
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) {
|
setValue(parsedDate: string) {
|
||||||
const splitValue = parsedDate.split('||');
|
const splitValue = parsedDate.split('||');
|
||||||
const fromValue = this.dateAdapter.parse(splitValue[0], this.datetimePickerFormat);
|
const fromValue = this.dateAdapter.parse(splitValue[0], this.datetimePickerFormat);
|
||||||
@@ -166,7 +178,7 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
this.submitValues();
|
this.submitValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
clear() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
from: '',
|
from: '',
|
||||||
@@ -174,11 +186,26 @@ export class SearchDatetimeRangeComponent implements SearchWidget, OnInit, OnDes
|
|||||||
});
|
});
|
||||||
if (this.id && this.context) {
|
if (this.id && this.context) {
|
||||||
this.context.queryFragments[this.id] = '';
|
this.context.queryFragments[this.id] = '';
|
||||||
this.context.update();
|
}
|
||||||
|
|
||||||
|
if (this.id && this.context && this.enableChangeUpdate) {
|
||||||
|
this.updateQuery();
|
||||||
}
|
}
|
||||||
this.setFromMaxDatetime();
|
this.setFromMaxDatetime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.clear();
|
||||||
|
this.updateQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateQuery() {
|
||||||
|
if (this.id && this.context) {
|
||||||
|
this.updateDisplayValue();
|
||||||
|
this.context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChangedHandler(event: any, formControl: FormControl) {
|
onChangedHandler(event: any, formControl: FormControl) {
|
||||||
|
|
||||||
const inputValue = event.value;
|
const inputValue = event.value;
|
||||||
|
|||||||
+49
@@ -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>
|
||||||
+89
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
@@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
+129
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -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>
|
||||||
+66
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+67
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -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>
|
||||||
+66
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+413
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
+38
@@ -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) {}
|
||||||
|
|
||||||
|
}
|
||||||
+22
@@ -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>
|
||||||
+39
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+31
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -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>
|
||||||
+68
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+68
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { SearchService, setupTestBed, AlfrescoApiService } from '@alfresco/adf-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 { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||||
import { fakeNodePaging } from './../../../mock/document-list.component.mock';
|
import { fakeNodePaging } from './../../../mock/document-list.component.mock';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ import {
|
|||||||
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
|
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
|
||||||
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
import { DataColumn, TranslationService } from '@alfresco/adf-core';
|
||||||
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
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 { SearchCategory } from '../../models/search-category.interface';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|||||||
+5
-62
@@ -1,11 +1,11 @@
|
|||||||
<mat-accordion multi="true" displayMode="flat">
|
<mat-accordion multi="true" displayMode="flat">
|
||||||
|
|
||||||
<button *ngIf="displayResetButton && responseFacets"
|
<button *ngIf="displayResetButton && facetFiltersService.responseFacets"
|
||||||
mat-button
|
mat-button
|
||||||
color="primary"
|
color="primary"
|
||||||
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.TOOLTIP' | translate }}"
|
matTooltip="{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.TOOLTIP' | translate }}"
|
||||||
matTooltipPosition="right"
|
matTooltipPosition="right"
|
||||||
(click)="resetAll()">
|
adf-reset-search>
|
||||||
{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.LABEL' | translate }}
|
{{ 'SEARCH.FILTER.BUTTONS.RESET-ALL.LABEL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<mat-expansion-panel
|
<mat-expansion-panel
|
||||||
@@ -24,72 +24,15 @@
|
|||||||
</adf-search-widget-container>
|
</adf-search-widget-container>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<ng-container *ngIf="responseFacets && showContextFacets">
|
<ng-container *ngIf="facetFiltersService.responseFacets && showContextFacets">
|
||||||
<mat-expansion-panel [attr.data-automation-id]="'expansion-panel-'+field.label" *ngFor="let field of responseFacets"
|
<mat-expansion-panel [attr.data-automation-id]="'expansion-panel-'+field.label" *ngFor="let field of facetFiltersService.responseFacets"
|
||||||
[expanded]="shouldExpand(field)">
|
[expanded]="shouldExpand(field)">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>{{ field.label | translate }}</mat-panel-title>
|
<mat-panel-title>{{ field.label | translate }}</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
<div class="adf-facet-result-filter">
|
<adf-search-facet-field [field]="field"></adf-search-facet-field>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
</mat-expansion-panel>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|||||||
-51
@@ -2,57 +2,6 @@
|
|||||||
$foreground: map-get($theme, foreground);
|
$foreground: map-get($theme, foreground);
|
||||||
|
|
||||||
.adf-search-filter {
|
.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 {
|
.mat-expansion-panel-header-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: mat-color($foreground, text, 0.87);
|
color: mat-color($foreground, text, 0.87);
|
||||||
|
|||||||
+66
-555
@@ -16,11 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchFilterComponent } from './search-filter.component';
|
import { SearchFilterComponent } from './search-filter.component';
|
||||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||||
import { AppConfigService, SearchService, setupTestBed, TranslationService } from '@alfresco/adf-core';
|
import { AppConfigService, SearchService, TranslationService } from '@alfresco/adf-core';
|
||||||
import { Subject } from 'rxjs';
|
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 { SearchFilterList } from '../../models/search-filter-list.model';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -39,6 +37,8 @@ import {
|
|||||||
stepTwo
|
stepTwo
|
||||||
} from '../../../mock';
|
} from '../../../mock';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
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', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let fixture: ComponentFixture<SearchFilterComponent>;
|
let fixture: ComponentFixture<SearchFilterComponent>;
|
||||||
@@ -48,8 +48,10 @@ describe('SearchFilterComponent', () => {
|
|||||||
const searchMock: any = {
|
const searchMock: any = {
|
||||||
dataLoaded: new Subject()
|
dataLoaded: new Subject()
|
||||||
};
|
};
|
||||||
|
let searchFacetFiltersService: SearchFacetFiltersService;
|
||||||
|
|
||||||
setupTestBed({
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
ContentTestingModule
|
ContentTestingModule
|
||||||
@@ -58,8 +60,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
{ provide: SearchService, useValue: searchMock }
|
{ provide: SearchService, useValue: searchMock }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
searchFacetFiltersService = TestBed.inject(SearchFacetFiltersService);
|
||||||
beforeEach(() => {
|
|
||||||
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
queryBuilder = TestBed.inject(SearchQueryBuilderService);
|
||||||
fixture = TestBed.createComponent(SearchFilterComponent);
|
fixture = TestBed.createComponent(SearchFilterComponent);
|
||||||
appConfigService = TestBed.inject(AppConfigService);
|
appConfigService = TestBed.inject(AppConfigService);
|
||||||
@@ -73,346 +74,6 @@ describe('SearchFilterComponent', () => {
|
|||||||
describe('component', () => {
|
describe('component', () => {
|
||||||
beforeEach(() => fixture.detectChanges());
|
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', () => {
|
it('should fetch facet fields from response payload and show the already checked items', () => {
|
||||||
spyOn(queryBuilder, 'execute').and.stub();
|
spyOn(queryBuilder, 'execute').and.stub();
|
||||||
queryBuilder.config = {
|
queryBuilder.config = {
|
||||||
@@ -426,13 +87,13 @@ describe('SearchFilterComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
component.responseFacets = <any> [
|
searchFacetFiltersService.responseFacets = <any> [
|
||||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
{ 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 = [
|
const serverResponseFields: any = [
|
||||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
{ 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);
|
fixture.detectChanges();
|
||||||
expect(component.responseFacets.length).toEqual(2);
|
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||||
expect(component.responseFacets[0].buckets.items[0].checked).toEqual(true, 'should show the already checked item');
|
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', () => {
|
it('should fetch facet fields from response payload and show the newly checked items', () => {
|
||||||
@@ -466,13 +131,13 @@ describe('SearchFilterComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
component.responseFacets = <any> [
|
searchFacetFiltersService.responseFacets = <any> [
|
||||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
{ 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 = [
|
const serverResponseFields: any = [
|
||||||
{ type: 'field', label: 'f1', field: 'f1', buckets: [
|
{ 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);
|
fixture.detectChanges();
|
||||||
expect(component.responseFacets.length).toEqual(2);
|
const facetField: SearchFacetFieldComponent = fixture.debugElement.query(By.css('adf-search-facet-field')).componentInstance;
|
||||||
expect(component.responseFacets[0].buckets.items[1].checked).toEqual(true, 'should show the newly checked item');
|
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', () => {
|
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> [
|
searchFacetFiltersService.responseFacets = <any> [
|
||||||
{ type: 'field', label: 'f1', field: 'f1', buckets: {items: [
|
{ type: 'field', label: 'f1', field: 'f1', buckets: new SearchFilterList([
|
||||||
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
{ label: 'b1', count: 10, filterQuery: 'filter', checked: true },
|
||||||
{ label: 'b2', count: 1, filterQuery: 'filter2' }] }},
|
{ label: 'b2', count: 1, filterQuery: 'filter2' }]) },
|
||||||
{ type: 'field', label: 'f2', field: 'f2', buckets: {items: [] }}
|
{ 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 = {
|
const data = {
|
||||||
list: {
|
list: {
|
||||||
context: {}
|
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);
|
fixture.detectChanges();
|
||||||
expect(component.responseFacets[0].buckets.items[1].count).toEqual(0);
|
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', () => {
|
expect(searchFacetFiltersService.responseFacets[0].buckets.items[0].count).toEqual(0);
|
||||||
spyOn(queryBuilder, 'update').and.stub();
|
expect(searchFacetFiltersService.responseFacets[0].buckets.items[1].count).toEqual(0);
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update query builder upon resetting selected queries', () => {
|
it('should update query builder upon resetting selected queries', () => {
|
||||||
@@ -607,109 +203,24 @@ describe('SearchFilterComponent', () => {
|
|||||||
|
|
||||||
const queryResponse = <any> {
|
const queryResponse = <any> {
|
||||||
label: 'query response',
|
label: 'query response',
|
||||||
buckets: <any> {
|
buckets: new SearchFilterList([
|
||||||
items: [
|
|
||||||
{ label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] },
|
{ label: 'q1', query: 'q1', checked: true, metrics: [{value: {count: 1}}] },
|
||||||
{ label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] },
|
{ label: 'q2', query: 'q2', checked: false, metrics: [{value: {count: 1}}] },
|
||||||
{ label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }]
|
{ label: 'q3', query: 'q3', checked: true, metrics: [{value: {count: 1}}] }])
|
||||||
}};
|
};
|
||||||
component.responseFacets = [queryResponse];
|
searchFacetFiltersService.responseFacets = [queryResponse];
|
||||||
component.resetSelectedBuckets(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.removeUserFacetBucket).toHaveBeenCalledTimes(3);
|
||||||
expect(queryBuilder.update).toHaveBeenCalled();
|
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);
|
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', () => {
|
describe('widgets', () => {
|
||||||
@@ -919,8 +430,6 @@ describe('SearchFilterComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
spyOn(queryBuilder, 'update').and.stub();
|
spyOn(queryBuilder, 'update').and.stub();
|
||||||
spyOn(component, 'selectFacetBucket').and.callThrough();
|
|
||||||
spyOn(component, 'onToggleBucket').and.callThrough();
|
|
||||||
|
|
||||||
const inputElement = fixture.debugElement.query(By.css(`${panel} input`));
|
const inputElement = fixture.debugElement.query(By.css(`${panel} input`));
|
||||||
inputElement.nativeElement.value = 'Extra';
|
inputElement.nativeElement.value = 'Extra';
|
||||||
@@ -937,7 +446,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
filteredMenu = getAllMenus(`${panel} mat-checkbox`, fixture);
|
filteredMenu = getAllMenus(`${panel} mat-checkbox`, fixture);
|
||||||
expect(filteredMenu).toEqual(filteredResult);
|
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', {});
|
clearButton.triggerEventHandler('click', {});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -951,8 +460,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
const checkedOption = fixture.debugElement.query(By.css(`${panel} mat-checkbox.mat-checkbox-checked`));
|
const checkedOption = fixture.debugElement.query(By.css(`${panel} mat-checkbox.mat-checkbox-checked`));
|
||||||
expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)');
|
expect(checkedOption.nativeElement.innerText).toEqual('Extra Small (10239)');
|
||||||
|
|
||||||
expect(component.onToggleBucket).toHaveBeenCalledTimes(1);
|
expect(queryBuilder.update).toHaveBeenCalledTimes(1);
|
||||||
expect(component.selectFacetBucket).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve the filter state if other fields edited', () => {
|
it('should preserve the filter state if other fields edited', () => {
|
||||||
@@ -964,8 +472,6 @@ describe('SearchFilterComponent', () => {
|
|||||||
queryBuilder.executed.next(<any> mockSearchResult);
|
queryBuilder.executed.next(<any> mockSearchResult);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
spyOn(queryBuilder, 'update').and.stub();
|
spyOn(queryBuilder, 'update').and.stub();
|
||||||
spyOn(component, 'selectFacetBucket').and.callThrough();
|
|
||||||
spyOn(component, 'onToggleBucket').and.callThrough();
|
|
||||||
|
|
||||||
const inputElement = fixture.debugElement.query(By.css(`${panel1} input`));
|
const inputElement = fixture.debugElement.query(By.css(`${panel1} input`));
|
||||||
inputElement.nativeElement.value = 'my';
|
inputElement.nativeElement.value = 'my';
|
||||||
@@ -995,15 +501,20 @@ describe('SearchFilterComponent', () => {
|
|||||||
panel1CheckedOption = fixture.debugElement.query(By.css(`${panel1} mat-checkbox.mat-checkbox-checked`));
|
panel1CheckedOption = fixture.debugElement.query(By.css(`${panel1} mat-checkbox.mat-checkbox-checked`));
|
||||||
expect(panel1CheckedOption.nativeElement.innerText).toEqual('my1 (806)');
|
expect(panel1CheckedOption.nativeElement.innerText).toEqual('my1 (806)');
|
||||||
|
|
||||||
expect(component.onToggleBucket).toHaveBeenCalledTimes(2);
|
expect(queryBuilder.update).toHaveBeenCalledTimes(2);
|
||||||
expect(component.selectFacetBucket).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset the query fragments when reset All is clicked', () => {
|
it('should reset the query fragments when reset All is clicked', () => {
|
||||||
component.queryBuilder.queryFragments = { 'fragment1' : 'value1'};
|
component.queryBuilder.queryFragments = { 'fragment1' : 'value1'};
|
||||||
component.responseFacets = [];
|
appConfigService.config.search = searchFilter;
|
||||||
spyOn(queryBuilder, 'resetToDefaults').and.stub();
|
searchFacetFiltersService.responseFacets = [];
|
||||||
component.resetAll();
|
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(component.queryBuilder.queryFragments).toEqual({});
|
||||||
expect(queryBuilder.resetToDefaults).toHaveBeenCalled();
|
expect(queryBuilder.resetToDefaults).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-366
@@ -15,22 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, ViewEncapsulation, OnInit, OnDestroy, Inject, Input } from '@angular/core';
|
import { Component, Inject, Input, ViewEncapsulation } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||||
import { TranslationService, SearchService } from '@alfresco/adf-core';
|
|
||||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
|
||||||
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
import { FacetFieldBucket } from '../../models/facet-field-bucket.interface';
|
||||||
import { FacetField } from '../../models/facet-field.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';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
import { SearchFacetFiltersService } from '../../services/search-facet-filters.service';
|
||||||
export interface SelectedBucket {
|
|
||||||
field: FacetField;
|
|
||||||
bucket: FacetFieldBucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-filter',
|
selector: 'adf-search-filter',
|
||||||
@@ -38,36 +28,22 @@ export interface SelectedBucket {
|
|||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
host: { class: 'adf-search-filter' }
|
host: { class: 'adf-search-filter' }
|
||||||
})
|
})
|
||||||
export class SearchFilterComponent implements OnInit, OnDestroy {
|
export class SearchFilterComponent {
|
||||||
|
|
||||||
/** Toggles whether to show or not the context facet filters. */
|
/** Toggles whether to show or not the context facet filters. */
|
||||||
@Input()
|
@Input()
|
||||||
showContextFacets: boolean = true;
|
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';
|
facetQueriesLabel: string = 'Facet Queries';
|
||||||
facetExpanded = {
|
facetExpanded = {
|
||||||
'default': false
|
'default': false
|
||||||
};
|
};
|
||||||
displayResetButton: boolean;
|
displayResetButton: boolean;
|
||||||
selectedBuckets: SelectedBucket[] = [];
|
|
||||||
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
|
||||||
|
|
||||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
||||||
private searchService: SearchService,
|
public facetFiltersService: SearchFacetFiltersService) {
|
||||||
private translationService: TranslationService) {
|
|
||||||
if (queryBuilder.config && queryBuilder.config.facetQueries) {
|
if (queryBuilder.config && queryBuilder.config.facetQueries) {
|
||||||
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
|
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
|
||||||
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE;
|
|
||||||
this.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded;
|
this.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded;
|
||||||
}
|
}
|
||||||
if (queryBuilder.config && queryBuilder.config.facetFields) {
|
if (queryBuilder.config && queryBuilder.config.facetFields) {
|
||||||
@@ -77,350 +53,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
|
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
|
||||||
}
|
}
|
||||||
this.displayResetButton = this.queryBuilder.config && !!this.queryBuilder.config.resetButton;
|
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 {
|
shouldExpand(field: FacetField): boolean {
|
||||||
return this.facetExpanded[field.type] || this.facetExpanded['default'];
|
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 {
|
getBucketCountDisplay(bucket: FacetFieldBucket): string {
|
||||||
return bucket.count === null ? '' : `(${bucket.count})`;
|
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-7
@@ -1,8 +1,33 @@
|
|||||||
<mat-form-field floatLabel="always" *ngIf="searchForms.length">
|
<ng-container *ngIf="queryBuilder.searchForms | async as forms">
|
||||||
<mat-label>{{ 'SEARCH.FORMS' | translate }}</mat-label>
|
|
||||||
<mat-select [(value)]="selected" (selectionChange)="onSelectionChange($event.value)">
|
<ng-container *ngIf="forms.length === 1">
|
||||||
<mat-option *ngFor="let form of searchForms" [value]="form.index">
|
<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}}
|
{{form.name | translate}}
|
||||||
</mat-option>
|
</button>
|
||||||
</mat-select>
|
</mat-menu>
|
||||||
</mat-form-field>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-25
@@ -16,20 +16,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { SearchFormComponent } from './search-form.component';
|
import { SearchFormComponent } from './search-form.component';
|
||||||
import { setupTestBed } from '@alfresco/adf-core';
|
import { setupTestBed } from '@alfresco/adf-core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
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 { SearchForm } from '../../models/search-form.interface';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
describe('SearchFormComponent', () => {
|
describe('SearchFormComponent', () => {
|
||||||
let fixture: ComponentFixture<SearchFormComponent>;
|
let fixture: ComponentFixture<SearchFormComponent>;
|
||||||
let component: SearchFormComponent;
|
let component: SearchFormComponent;
|
||||||
let queryBuilder: SearchHeaderQueryBuilderService;
|
let queryBuilder: SearchQueryBuilderService;
|
||||||
const mockSearchForms: SearchForm[] = [
|
const mockSearchForms: SearchForm[] = [
|
||||||
{ default: false, index: 0, name: 'All', selected: false },
|
{ default: false, index: 0, name: 'All', selected: false },
|
||||||
{ default: true, index: 1, name: 'First', selected: true },
|
{ default: true, index: 1, name: 'First', selected: true },
|
||||||
@@ -42,50 +41,59 @@ describe('SearchFormComponent', () => {
|
|||||||
ContentTestingModule
|
ContentTestingModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService }
|
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchQueryBuilderService }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SearchFormComponent);
|
fixture = TestBed.createComponent(SearchFormComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
queryBuilder = TestBed.inject<SearchHeaderQueryBuilderService>(SEARCH_QUERY_SERVICE_TOKEN);
|
queryBuilder = TestBed.inject<SearchQueryBuilderService>(SEARCH_QUERY_SERVICE_TOKEN);
|
||||||
spyOn(queryBuilder, 'getSearchConfigurationDetails').and.returnValue(mockSearchForms);
|
queryBuilder.searchForms.next(mockSearchForms);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show search forms', async () => {
|
it('should show search forms', () => {
|
||||||
await fixture.whenStable();
|
const title = fixture.debugElement.query(By.css('.adf-search-form-title'));
|
||||||
fixture.detectChanges();
|
expect(title.nativeElement.innerText).toContain(mockSearchForms[1].name);
|
||||||
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 emit on form change', async (done) => {
|
it('should emit on form change', (done) => {
|
||||||
|
spyOn(queryBuilder, 'updateSelectedConfiguration').and.stub();
|
||||||
component.formChange.subscribe((form) => {
|
component.formChange.subscribe((form) => {
|
||||||
expect(form).toEqual(mockSearchForms[2]);
|
expect(form).toEqual(mockSearchForms[2]);
|
||||||
|
expect(queryBuilder.updateSelectedConfiguration).toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
await fixture.whenStable();
|
const button = fixture.debugElement.query(By.css('.adf-search-form')).nativeElement;
|
||||||
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const matSelect = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
const matOption = fixture.debugElement.queryAll(By.css('.mat-menu-item'))[2].nativeElement;
|
||||||
matSelect.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
const matOption = fixture.debugElement.queryAll(By.css('.mat-option'))[2].nativeElement;
|
|
||||||
matOption.click();
|
matOption.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display search form if no form configured', async () => {
|
it('should not show menu if only one config found', () => {
|
||||||
component.searchForms = [];
|
queryBuilder.searchForms.next([{ name: 'one', selected: true, default: true, index: 0 }]);
|
||||||
await fixture.whenStable();
|
|
||||||
fixture.detectChanges();
|
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');
|
expect(field).toEqual(null, 'search form displayed for empty configuration');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-14
@@ -15,30 +15,30 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Inject, Output, ViewEncapsulation } from '@angular/core';
|
||||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
|
||||||
import { SearchForm } from '../../models/search-form.interface';
|
import { SearchForm } from '../../models/search-form.interface';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-form',
|
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()
|
@Output()
|
||||||
formChange: EventEmitter<SearchForm> = new EventEmitter<SearchForm>();
|
formChange: EventEmitter<SearchForm> = new EventEmitter<SearchForm>();
|
||||||
|
|
||||||
selected: number;
|
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService) {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectionChange(index: number) {
|
onSelectionChange(form: SearchForm) {
|
||||||
this.formChange.emit(this.searchForms[index]);
|
this.queryBuilder.updateSelectedConfiguration(form.index);
|
||||||
|
this.formChange.emit(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelected(forms: SearchForm[]): string {
|
||||||
|
return forms.find((form) => form.selected)?.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -29,8 +29,8 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
|
||||||
<div class="adf-facet-buttons">
|
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
|
||||||
<button mat-button color="primary" type="button" (click)="reset()" data-automation-id="number-range-btn-clear">
|
<button mat-button color="primary" type="button" (click)="clear()" data-automation-id="number-range-btn-clear">
|
||||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="number-range-btn-apply">
|
<button mat-button color="primary" type="submit" [disabled]="!form.valid" data-automation-id="number-range-btn-apply">
|
||||||
|
|||||||
+30
-4
@@ -15,12 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OnInit, Component, ViewEncapsulation } from '@angular/core';
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-number-range',
|
selector: 'adf-search-number-range',
|
||||||
@@ -48,6 +49,8 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
|||||||
startValue: any;
|
startValue: any;
|
||||||
|
|
||||||
validators: Validators;
|
validators: Validators;
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
@@ -74,6 +77,9 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
|||||||
from: this.from,
|
from: this.from,
|
||||||
to: this.to
|
to: this.to
|
||||||
}, this.formValidator);
|
}, this.formValidator);
|
||||||
|
|
||||||
|
this.enableChangeUpdate = this.settings?.allowUpdateOnChange ?? true;
|
||||||
|
this.updateDisplayValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
formValidator(formGroup: FormGroup) {
|
formValidator(formGroup: FormGroup) {
|
||||||
@@ -82,6 +88,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
|||||||
|
|
||||||
apply(model: { from: string, to: string }, isValid: boolean) {
|
apply(model: { from: string, to: string }, isValid: boolean) {
|
||||||
if (isValid && this.id && this.context && this.field) {
|
if (isValid && this.id && this.context && this.field) {
|
||||||
|
this.updateDisplayValue();
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
|
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
@@ -118,12 +125,21 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
|||||||
return this.form.value;
|
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) {
|
setValue(value: any) {
|
||||||
this.form['from'].setValue(value);
|
this.form['from'].setValue(value);
|
||||||
this.form['to'].setValue(value);
|
this.form['to'].setValue(value);
|
||||||
|
this.updateDisplayValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
clear() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
@@ -133,6 +149,16 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
|
|||||||
|
|
||||||
if (this.id && this.context) {
|
if (this.id && this.context) {
|
||||||
this.context.queryFragments[this.id] = '';
|
this.context.queryFragments[this.id] = '';
|
||||||
|
this.updateDisplayValue();
|
||||||
|
if (this.enableChangeUpdate) {
|
||||||
|
this.context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.clear();
|
||||||
|
if (this.id && this.context) {
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
import { ContentNodeSelectorPanelService } from '../../../content-node-selector/content-node-selector-panel.service';
|
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';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
+32
-6
@@ -15,13 +15,14 @@
|
|||||||
* limitations under the License.
|
* 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 { MatRadioChange } from '@angular/material/radio';
|
||||||
|
|
||||||
import { SearchWidget } from '../../models/search-widget.interface';
|
import { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { SearchFilterList } from '../../models/search-filter-list.model';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
export interface SearchRadioOption {
|
export interface SearchRadioOption {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,6 +49,8 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
|||||||
pageSize = 5;
|
pageSize = 5;
|
||||||
isActive = false;
|
isActive = false;
|
||||||
startValue: any;
|
startValue: any;
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.options = new SearchFilterList<SearchRadioOption>();
|
this.options = new SearchFilterList<SearchRadioOption>();
|
||||||
@@ -73,6 +76,8 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
|||||||
this.value = initialValue;
|
this.value = initialValue;
|
||||||
this.context.queryFragments[this.id] = initialValue;
|
this.context.queryFragments[this.id] = initialValue;
|
||||||
}
|
}
|
||||||
|
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||||
|
this.updateDisplayValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSelectedValue(): string {
|
private getSelectedValue(): string {
|
||||||
@@ -90,8 +95,9 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submitValues() {
|
submitValues() {
|
||||||
const currentValue = this.getSelectedValue();
|
this.setValue(this.value);
|
||||||
this.setValue(currentValue);
|
this.updateDisplayValue();
|
||||||
|
this.context.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidValue() {
|
hasValidValue() {
|
||||||
@@ -102,23 +108,43 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
|||||||
setValue(newValue: string) {
|
setValue(newValue: string) {
|
||||||
this.value = newValue;
|
this.value = newValue;
|
||||||
this.context.queryFragments[this.id] = newValue;
|
this.context.queryFragments[this.id] = newValue;
|
||||||
|
if (this.enableChangeUpdate) {
|
||||||
|
this.updateDisplayValue();
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentValue() {
|
getCurrentValue() {
|
||||||
return this.getSelectedValue();
|
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) {
|
changeHandler(event: MatRadioChange) {
|
||||||
this.setValue(event.value);
|
this.setValue(event.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
clear() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
|
||||||
const initialValue = this.getSelectedValue();
|
const initialValue = this.getSelectedValue();
|
||||||
if (initialValue !== null) {
|
if (initialValue !== null) {
|
||||||
this.setValue(initialValue);
|
this.setValue(initialValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
const initialValue = this.getSelectedValue();
|
||||||
|
if (initialValue !== null) {
|
||||||
|
this.setValue(initialValue);
|
||||||
|
this.updateDisplayValue();
|
||||||
|
this.context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@
|
|||||||
data-automation-id="slider-range">
|
data-automation-id="slider-range">
|
||||||
</mat-slider>
|
</mat-slider>
|
||||||
|
|
||||||
<div class="adf-facet-buttons">
|
<div class="adf-facet-buttons" *ngIf="!settings?.hideDefaultAction">
|
||||||
<button mat-button color="primary" (click)="reset()" data-automation-id="slider-btn-clear">
|
<button mat-button color="primary" (click)="clear()" data-automation-id="slider-btn-clear">
|
||||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-2
@@ -74,14 +74,13 @@ describe('SearchSliderComponent', () => {
|
|||||||
component.context = context;
|
component.context = context;
|
||||||
component.id = 'contentSize';
|
component.id = 'contentSize';
|
||||||
component.settings = { field: 'cm:content.size' };
|
component.settings = { field: 'cm:content.size' };
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
component.onChangedHandler(<MatSliderChange> { value: 10 });
|
component.onChangedHandler(<MatSliderChange> { value: 10 });
|
||||||
fixture.detectChanges();
|
|
||||||
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
|
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 10]');
|
||||||
expect(context.update).toHaveBeenCalled();
|
expect(context.update).toHaveBeenCalled();
|
||||||
|
|
||||||
component.onChangedHandler(<MatSliderChange> { value: 20 });
|
component.onChangedHandler(<MatSliderChange> { value: 20 });
|
||||||
fixture.detectChanges();
|
|
||||||
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]');
|
expect(context.queryFragments[component.id]).toEqual('cm:content.size:[0 TO 20]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -15,11 +15,12 @@
|
|||||||
* limitations under the License.
|
* 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 { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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 { MatSliderChange } from '@angular/material/slider';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-slider',
|
selector: 'adf-search-slider',
|
||||||
@@ -39,6 +40,8 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
|||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
thumbLabel = false;
|
thumbLabel = false;
|
||||||
|
enableChangeUpdate: boolean;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
/** The numeric value represented by the slider. */
|
/** The numeric value represented by the slider. */
|
||||||
@Input()
|
@Input()
|
||||||
@@ -59,6 +62,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.thumbLabel = this.settings['thumbLabel'] ? true : false;
|
this.thumbLabel = this.settings['thumbLabel'] ? true : false;
|
||||||
|
this.enableChangeUpdate = this.settings.allowUpdateOnChange ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.startValue) {
|
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() {
|
reset() {
|
||||||
this.value = this.min || 0;
|
this.value = this.min || 0;
|
||||||
this.updateQuery(null);
|
this.updateQuery(null);
|
||||||
@@ -73,8 +84,10 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
|||||||
|
|
||||||
onChangedHandler(event: MatSliderChange) {
|
onChangedHandler(event: MatSliderChange) {
|
||||||
this.value = event.value;
|
this.value = event.value;
|
||||||
|
if (this.enableChangeUpdate) {
|
||||||
this.updateQuery(this.value);
|
this.updateQuery(this.value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
submitValues() {
|
submitValues() {
|
||||||
this.updateQuery(this.value);
|
this.updateQuery(this.value);
|
||||||
@@ -94,6 +107,7 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateQuery(value: number | null) {
|
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 (this.id && this.context && this.settings && this.settings.field) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
this.context.queryFragments[this.id] = '';
|
this.context.queryFragments[this.id] = '';
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchSortingPickerComponent } from './search-sorting-picker.component';
|
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 { AppConfigService } from '@alfresco/adf-core';
|
||||||
import { SearchConfiguration } from '../../models/search-configuration.interface';
|
import { SearchConfiguration } from '../../models/search-configuration.interface';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, ViewEncapsulation, Inject } from '@angular/core';
|
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 { SearchSortingDefinition } from '../../models/search-sorting-definition.interface';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
placeholder="{{ settings?.placeholder | translate }}"
|
placeholder="{{ settings?.placeholder | translate }}"
|
||||||
[(ngModel)]="value"
|
[(ngModel)]="value"
|
||||||
(change)="onChangedHandler($event)">
|
(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>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|||||||
+13
-4
@@ -15,10 +15,11 @@
|
|||||||
* limitations under the License.
|
* 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 { SearchWidget } from '../../models/search-widget.interface';
|
||||||
import { SearchWidgetSettings } from '../../models/search-widget-settings.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({
|
@Component({
|
||||||
selector: 'adf-search-text',
|
selector: 'adf-search-text',
|
||||||
@@ -39,6 +40,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
|||||||
startValue: string;
|
startValue: string;
|
||||||
isActive = false;
|
isActive = false;
|
||||||
enableChangeUpdate = true;
|
enableChangeUpdate = true;
|
||||||
|
displayValue$: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.context && this.settings && this.settings.pattern) {
|
if (this.context && this.settings && this.settings.pattern) {
|
||||||
@@ -59,9 +61,15 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
clear() {
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
|
this.value = '';
|
||||||
|
if (this.enableChangeUpdate) {
|
||||||
|
this.updateQuery(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.updateQuery(null);
|
this.updateQuery(null);
|
||||||
}
|
}
|
||||||
@@ -75,11 +83,11 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateQuery(value: string) {
|
private updateQuery(value: string) {
|
||||||
|
this.displayValue$.next(value);
|
||||||
if (this.context && this.settings && this.settings.field) {
|
if (this.context && this.settings && this.settings.field) {
|
||||||
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
|
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
|
||||||
this.context.update();
|
this.context.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitValues() {
|
submitValues() {
|
||||||
@@ -96,6 +104,7 @@ export class SearchTextComponent implements SearchWidget, OnInit {
|
|||||||
|
|
||||||
setValue(value: string) {
|
setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
this.displayValue$.next(this.value);
|
||||||
this.submitValues();
|
this.submitValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-4
@@ -15,10 +15,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, ComponentRef, ComponentFactoryResolver, Inject, SimpleChanges, OnChanges } from '@angular/core';
|
import {
|
||||||
import { SearchFilterService } from '../search-filter/search-filter.service';
|
Component,
|
||||||
import { BaseQueryBuilderService } from '../../base-query-builder.service';
|
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 { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-widget-container',
|
selector: 'adf-search-widget-container',
|
||||||
@@ -74,7 +87,7 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan
|
|||||||
private setupWidget(ref: ComponentRef<any>) {
|
private setupWidget(ref: ComponentRef<any>) {
|
||||||
if (ref && ref.instance) {
|
if (ref && ref.instance) {
|
||||||
ref.instance.id = this.id;
|
ref.instance.id = this.id;
|
||||||
ref.instance.settings = { ...this.settings };
|
ref.instance.settings = {...this.settings};
|
||||||
ref.instance.context = this.queryBuilder;
|
ref.instance.context = this.queryBuilder;
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
ref.instance.isActive = true;
|
ref.instance.isActive = true;
|
||||||
@@ -107,6 +120,13 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy, OnChan
|
|||||||
return this.componentRef.instance.getCurrentValue();
|
return this.componentRef.instance.getCurrentValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisplayValue(): Observable<string> | null {
|
||||||
|
if (!this.componentRef?.instance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.componentRef.instance.displayValue$;
|
||||||
|
}
|
||||||
|
|
||||||
resetInnerWidget() {
|
resetInnerWidget() {
|
||||||
if (this.componentRef && this.componentRef.instance) {
|
if (this.componentRef && this.componentRef.instance) {
|
||||||
this.componentRef.instance.reset();
|
this.componentRef.instance.reset();
|
||||||
|
|||||||
@@ -31,5 +31,13 @@ export interface FacetField {
|
|||||||
currentPageSize?: number;
|
currentPageSize?: number;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
settings?: FacetFieldSettings;
|
||||||
[propName: string]: any;
|
[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 { FilterQuery } from './filter-query.interface';
|
||||||
import { FacetQuery } from './facet-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 { SearchCategory } from './search-category.interface';
|
||||||
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
||||||
import { RequestHighlight } from '@alfresco/js-api';
|
import { RequestHighlight } from '@alfresco/js-api';
|
||||||
@@ -35,6 +35,7 @@ export interface SearchConfiguration {
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
mincount?: number;
|
mincount?: number;
|
||||||
queries: FacetQuery[];
|
queries: FacetQuery[];
|
||||||
|
settings?: FacetFieldSettings;
|
||||||
};
|
};
|
||||||
facetFields?: {
|
facetFields?: {
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
|
|||||||
@@ -17,5 +17,14 @@
|
|||||||
|
|
||||||
export interface SearchWidgetSettings {
|
export interface SearchWidgetSettings {
|
||||||
field: string;
|
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;
|
[indexer: string]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchWidgetSettings } from './search-widget-settings.interface';
|
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 {
|
export interface SearchWidget {
|
||||||
id: string;
|
id: string;
|
||||||
|
/* optional field control options */
|
||||||
settings?: SearchWidgetSettings;
|
settings?: SearchWidgetSettings;
|
||||||
context?: SearchQueryBuilderService;
|
context?: SearchQueryBuilderService;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
startValue: any;
|
startValue: any;
|
||||||
reset();
|
/* stream emit value on changes */
|
||||||
submitValues();
|
displayValue$: Subject<string>;
|
||||||
hasValidValue();
|
/* reset the value and update the search */
|
||||||
getCurrentValue();
|
reset(): void;
|
||||||
|
/* update the search with field value */
|
||||||
|
submitValues(): void;
|
||||||
|
hasValidValue(): boolean;
|
||||||
|
getCurrentValue(): any;
|
||||||
setValue(value: 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-settings.interface';
|
||||||
export * from './models/search-widget.interface';
|
export * from './models/search-widget.interface';
|
||||||
export * from './models/search-configuration.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-range.interface';
|
||||||
export * from './models/search-form.interface';
|
export * from './models/search-form.interface';
|
||||||
|
|
||||||
export * from './search-query-service.token';
|
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.component';
|
||||||
export * from './components/search-control.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-chip-list/search-chip-list.component';
|
||||||
export * from './components/search-date-range/search-date-range.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.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-filter-container/search-filter-container.component';
|
||||||
export * from './components/search-number-range/search-number-range.component';
|
export * from './components/search-number-range/search-number-range.component';
|
||||||
export * from './components/search-radio/search-radio.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-widget-container/search-widget-container.component';
|
||||||
export * from './components/search-datetime-range/search-datetime-range.component';
|
export * from './components/search-datetime-range/search-datetime-range.component';
|
||||||
export * from './components/search-form/search-form.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';
|
export * from './search.module';
|
||||||
|
|||||||
@@ -16,6 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { InjectionToken } from '@angular/core';
|
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');
|
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 { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component';
|
||||||
import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component';
|
import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component';
|
||||||
import { SEARCH_QUERY_SERVICE_TOKEN } from './search-query-service.token';
|
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 { SearchFilterContainerComponent } from './components/search-filter-container/search-filter-container.component';
|
||||||
import { SearchDatetimeRangeComponent } from './components/search-datetime-range/search-datetime-range.component';
|
import { SearchDatetimeRangeComponent } from './components/search-datetime-range/search-datetime-range.component';
|
||||||
import { SearchFormComponent } from './components/search-form/search-form.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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -67,7 +73,13 @@ import { SearchFormComponent } from './components/search-form/search-form.compon
|
|||||||
SearchDatetimeRangeComponent,
|
SearchDatetimeRangeComponent,
|
||||||
SearchSortingPickerComponent,
|
SearchSortingPickerComponent,
|
||||||
SearchFilterContainerComponent,
|
SearchFilterContainerComponent,
|
||||||
SearchFormComponent
|
SearchFormComponent,
|
||||||
|
SearchFilterChipsComponent,
|
||||||
|
SearchFilterMenuCardComponent,
|
||||||
|
SearchFacetFieldComponent,
|
||||||
|
SearchWidgetChipComponent,
|
||||||
|
SearchFacetChipComponent,
|
||||||
|
ResetSearchDirective
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
@@ -86,11 +98,14 @@ import { SearchFormComponent } from './components/search-form/search-form.compon
|
|||||||
SearchDatetimeRangeComponent,
|
SearchDatetimeRangeComponent,
|
||||||
SearchSortingPickerComponent,
|
SearchSortingPickerComponent,
|
||||||
SearchFilterContainerComponent,
|
SearchFilterContainerComponent,
|
||||||
SearchFormComponent
|
SearchFormComponent,
|
||||||
|
SearchFilterChipsComponent,
|
||||||
|
SearchFilterMenuCardComponent,
|
||||||
|
SearchFacetFieldComponent,
|
||||||
|
ResetSearchDirective
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService },
|
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService }
|
||||||
SearchSortingPickerComponent
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchModule {}
|
export class SearchModule {}
|
||||||
|
|||||||
+23
-14
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
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 { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||||
import {
|
import {
|
||||||
QueryBody,
|
QueryBody,
|
||||||
@@ -27,15 +27,15 @@ import {
|
|||||||
RequestHighlight,
|
RequestHighlight,
|
||||||
RequestScope
|
RequestScope
|
||||||
} from '@alfresco/js-api';
|
} from '@alfresco/js-api';
|
||||||
import { SearchCategory } from './models/search-category.interface';
|
import { SearchCategory } from '../models/search-category.interface';
|
||||||
import { FilterQuery } from './models/filter-query.interface';
|
import { FilterQuery } from '../models/filter-query.interface';
|
||||||
import { SearchRange } from './models/search-range.interface';
|
import { SearchRange } from '../models/search-range.interface';
|
||||||
import { SearchConfiguration } from './models/search-configuration.interface';
|
import { SearchConfiguration } from '../models/search-configuration.interface';
|
||||||
import { FacetQuery } from './models/facet-query.interface';
|
import { FacetQuery } from '../models/facet-query.interface';
|
||||||
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
|
import { SearchSortingDefinition } from '../models/search-sorting-definition.interface';
|
||||||
import { FacetField } from './models/facet-field.interface';
|
import { FacetField } from '../models/facet-field.interface';
|
||||||
import { FacetFieldBucket } from './models/facet-field-bucket.interface';
|
import { FacetFieldBucket } from '../models/facet-field-bucket.interface';
|
||||||
import { SearchForm } from './models/search-form.interface';
|
import { SearchForm } from '../models/search-form.interface';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -54,6 +54,9 @@ export abstract class BaseQueryBuilderService {
|
|||||||
/* Stream that emits the error whenever user search */
|
/* Stream that emits the error whenever user search */
|
||||||
error = new Subject();
|
error = new Subject();
|
||||||
|
|
||||||
|
/* Stream that emits search forms */
|
||||||
|
searchForms = new ReplaySubject<SearchForm[]>(1);
|
||||||
|
|
||||||
categories: SearchCategory[] = [];
|
categories: SearchCategory[] = [];
|
||||||
queryFragments: { [id: string]: string } = {};
|
queryFragments: { [id: string]: string } = {};
|
||||||
filterQueries: FilterQuery[] = [];
|
filterQueries: FilterQuery[] = [];
|
||||||
@@ -92,14 +95,16 @@ export abstract class BaseQueryBuilderService {
|
|||||||
|
|
||||||
public resetToDefaults() {
|
public resetToDefaults() {
|
||||||
const currentConfig = this.getDefaultConfiguration();
|
const currentConfig = this.getDefaultConfiguration();
|
||||||
|
this.resetSearchOptions();
|
||||||
this.configUpdated.next(currentConfig);
|
this.configUpdated.next(currentConfig);
|
||||||
|
this.searchForms.next(this.getSearchFormDetails());
|
||||||
this.setUpSearchConfiguration(currentConfig);
|
this.setUpSearchConfiguration(currentConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDefaultConfiguration(): SearchConfiguration | undefined {
|
public getDefaultConfiguration(): SearchConfiguration | undefined {
|
||||||
const configurations = this.loadConfiguration();
|
const configurations = this.loadConfiguration();
|
||||||
|
|
||||||
if (this.selectedConfiguration >= 0) {
|
if (this.selectedConfiguration !== undefined) {
|
||||||
return configurations[this.selectedConfiguration];
|
return configurations[this.selectedConfiguration];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +117,9 @@ export abstract class BaseQueryBuilderService {
|
|||||||
public updateSelectedConfiguration(index: number): void {
|
public updateSelectedConfiguration(index: number): void {
|
||||||
const currentConfig = this.loadConfiguration();
|
const currentConfig = this.loadConfiguration();
|
||||||
if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) {
|
if (Array.isArray(currentConfig) && currentConfig[index] !== undefined) {
|
||||||
this.configUpdated.next(currentConfig[index]);
|
|
||||||
this.selectedConfiguration = index;
|
this.selectedConfiguration = index;
|
||||||
|
this.configUpdated.next(currentConfig[index]);
|
||||||
|
this.searchForms.next(this.getSearchFormDetails());
|
||||||
this.resetSearchOptions();
|
this.resetSearchOptions();
|
||||||
this.setUpSearchConfiguration(currentConfig[index]);
|
this.setUpSearchConfiguration(currentConfig[index]);
|
||||||
this.update();
|
this.update();
|
||||||
@@ -126,18 +132,21 @@ export abstract class BaseQueryBuilderService {
|
|||||||
this.filterQueries = [];
|
this.filterQueries = [];
|
||||||
this.sorting = [];
|
this.sorting = [];
|
||||||
this.sortingOptions = [];
|
this.sortingOptions = [];
|
||||||
|
this.userFacetBuckets = {};
|
||||||
this.scope = null;
|
this.scope = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSearchConfigurationDetails(): SearchForm[] {
|
public getSearchFormDetails(): SearchForm[] {
|
||||||
const configurations = this.loadConfiguration();
|
const configurations = this.loadConfiguration();
|
||||||
if (Array.isArray(configurations)) {
|
if (Array.isArray(configurations)) {
|
||||||
return configurations.map((configuration, index) => ({
|
return configurations.map((configuration, index) => ({
|
||||||
index,
|
index,
|
||||||
name: configuration.name || 'SEARCH.UNKNOWN_FORM',
|
name: configuration.name || 'SEARCH.UNKNOWN_CONFIGURATION',
|
||||||
default: configuration.default || false,
|
default: configuration.default || false,
|
||||||
selected: this.selectedConfiguration !== undefined ? index === this.selectedConfiguration : configuration.default
|
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 [];
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-7
@@ -16,13 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Type } from '@angular/core';
|
import { Injectable, Type } from '@angular/core';
|
||||||
import { SearchTextComponent } from '../search-text/search-text.component';
|
import { SearchTextComponent } from '../components/search-text/search-text.component';
|
||||||
import { SearchRadioComponent } from '../search-radio/search-radio.component';
|
import { SearchRadioComponent } from '../components/search-radio/search-radio.component';
|
||||||
import { SearchSliderComponent } from '../search-slider/search-slider.component';
|
import { SearchSliderComponent } from '../components/search-slider/search-slider.component';
|
||||||
import { SearchNumberRangeComponent } from '../search-number-range/search-number-range.component';
|
import { SearchNumberRangeComponent } from '../components/search-number-range/search-number-range.component';
|
||||||
import { SearchCheckListComponent } from '../search-check-list/search-check-list.component';
|
import { SearchCheckListComponent } from '../components/search-check-list/search-check-list.component';
|
||||||
import { SearchDateRangeComponent } from '../search-date-range/search-date-range.component';
|
import { SearchDateRangeComponent } from '../components/search-date-range/search-date-range.component';
|
||||||
import { SearchDatetimeRangeComponent } from '../search-datetime-range/search-datetime-range.component';
|
import { SearchDatetimeRangeComponent } from '../components/search-datetime-range/search-datetime-range.component';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
+2
-2
@@ -15,11 +15,11 @@
|
|||||||
* limitations under the License.
|
* 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 { AppConfigService } from '@alfresco/adf-core';
|
||||||
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
|
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { ContentTestingModule } from '../testing/content.testing.module';
|
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||||
|
|
||||||
describe('SearchHeaderQueryBuilderService', () => {
|
describe('SearchHeaderQueryBuilderService', () => {
|
||||||
|
|
||||||
+4
-4
@@ -17,14 +17,14 @@
|
|||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-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 { 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 { MinimalNode, QueryBody } from '@alfresco/js-api';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { SearchSortingDefinition } from './models/search-sorting-definition.interface';
|
import { SearchSortingDefinition } from '../models/search-sorting-definition.interface';
|
||||||
import { FilterSearch } from './models/filter-search.interface';
|
import { FilterSearch } from '../models/filter-search.interface';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
+18
-14
@@ -16,11 +16,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
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 { 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 { TestBed } from '@angular/core/testing';
|
||||||
import { ContentTestingModule } from '../testing/content.testing.module';
|
import { ContentTestingModule } from '../../testing/content.testing.module';
|
||||||
|
|
||||||
describe('SearchQueryBuilder', () => {
|
describe('SearchQueryBuilder', () => {
|
||||||
|
|
||||||
@@ -670,10 +670,12 @@ describe('SearchQueryBuilder', () => {
|
|||||||
expect(queryBody.scope).toEqual(mockScope);
|
expect(queryBody.scope).toEqual(mockScope);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty if array of search config not found', () => {
|
it('should return empty if array of search config not found', (done) => {
|
||||||
const builder = new SearchQueryBuilderService(buildConfig({}), null);
|
const builder = new SearchQueryBuilderService(buildConfig(null), null);
|
||||||
const forms = builder.getSearchConfigurationDetails();
|
builder.searchForms.subscribe((forms) => {
|
||||||
expect(forms).toEqual([]);
|
expect(forms).toEqual([]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Multiple search configuration', () => {
|
describe('Multiple search configuration', () => {
|
||||||
@@ -728,14 +730,15 @@ describe('SearchQueryBuilder', () => {
|
|||||||
expect(builder.filterQueries.length).toBe(2);
|
expect(builder.filterQueries.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list available search form names', () => {
|
it('should list available search form names', (done) => {
|
||||||
const forms = builder.getSearchConfigurationDetails();
|
builder.searchForms.subscribe((forms) => {
|
||||||
|
|
||||||
expect(forms).toEqual([
|
expect(forms).toEqual([
|
||||||
{ index: 0, name: 'config1', default: true, selected: true },
|
{ index: 0, name: 'config1', default: true, selected: true },
|
||||||
{ index: 1, name: 'config2', default: false, selected: false },
|
{ index: 1, name: 'config2', default: false, selected: false },
|
||||||
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
|
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
|
||||||
]);
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow the user switch the form', () => {
|
it('should allow the user switch the form', () => {
|
||||||
@@ -745,15 +748,16 @@ describe('SearchQueryBuilder', () => {
|
|||||||
expect(builder.filterQueries.length).toBe(2);
|
expect(builder.filterQueries.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep the selected configuration value', () => {
|
it('should keep the selected configuration value', (done) => {
|
||||||
builder.updateSelectedConfiguration(1);
|
builder.updateSelectedConfiguration(1);
|
||||||
const forms = builder.getSearchConfigurationDetails();
|
builder.searchForms.subscribe((forms) => {
|
||||||
|
|
||||||
expect(forms).toEqual([
|
expect(forms).toEqual([
|
||||||
{ index: 0, name: 'config1', default: true, selected: false },
|
{ index: 0, name: 'config1', default: true, selected: false },
|
||||||
{ index: 1, name: 'config2', default: false, selected: true },
|
{ index: 1, name: 'config2', default: false, selected: true },
|
||||||
{ index: 2, name: 'SEARCH.UNKNOWN_FORM', default: false, selected: false }
|
{ index: 2, name: 'SEARCH.UNKNOWN_CONFIGURATION', default: false, selected: false }
|
||||||
]);
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-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';
|
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -28,6 +28,10 @@
|
|||||||
@import '../aspect-list/aspect-list.component';
|
@import '../aspect-list/aspect-list.component';
|
||||||
@import '../permission-manager/components/user-icon-column/user-icon-column.component';
|
@import '../permission-manager/components/user-icon-column/user-icon-column.component';
|
||||||
@import '../permission-manager/components/user-name-column/user-name-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) {
|
@mixin adf-content-services-theme($theme) {
|
||||||
@include adf-breadcrumb-theme($theme);
|
@include adf-breadcrumb-theme($theme);
|
||||||
@@ -56,4 +60,8 @@
|
|||||||
@include adf-version-comparison-theme($theme);
|
@include adf-version-comparison-theme($theme);
|
||||||
@include adf-content-type-dialog-theme($theme);
|
@include adf-content-type-dialog-theme($theme);
|
||||||
@include adf-aspect-list-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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,6 +499,26 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"search-widget-setting": {
|
||||||
|
"description": "Search widget setting",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allowUpdateOnChange": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "update search query with every user changes on widget"
|
||||||
|
},
|
||||||
|
"hideDefaultAction": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Hides the widget action i.e clear and submit"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "unit type of the widget value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"search-configuration": {
|
"search-configuration": {
|
||||||
"description": "Search configuration parameters",
|
"description": "Search configuration parameters",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -589,6 +609,9 @@
|
|||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"$ref": "#/definitions/search-widget-setting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,6 +683,9 @@
|
|||||||
"mincount": {
|
"mincount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1."
|
"description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"$ref": "#/definitions/search-widget-setting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,6 +742,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"$ref": "#/definitions/search-widget-setting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -750,8 +779,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"description": "Component-specific settings",
|
"$ref": "#/definitions/search-widget-setting"
|
||||||
"type": "object"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,4 +139,5 @@ export class DateRangeFilterPage {
|
|||||||
async checkClearButtonIsDisplayed(): Promise<void> {
|
async checkClearButtonIsDisplayed(): Promise<void> {
|
||||||
await BrowserVisibility.waitUntilElementIsVisible(this.filter.element(this.clearButton));
|
await BrowserVisibility.waitUntilElementIsVisible(this.filter.element(this.clearButton));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ echo "====== Copy i18n ======"
|
|||||||
mkdir -p ./lib/dist/core/bundles/assets/adf-core/i18n
|
mkdir -p ./lib/dist/core/bundles/assets/adf-core/i18n
|
||||||
cp -R ./lib/core/i18n/* ./lib/dist/core/bundles/assets/adf-core/i18n
|
cp -R ./lib/core/i18n/* ./lib/dist/core/bundles/assets/adf-core/i18n
|
||||||
|
|
||||||
|
echo "====== Copy schema ======"
|
||||||
|
cp -R ./lib/core/app-config/schema.json lib/dist/core/app.config.schema.json
|
||||||
|
|
||||||
echo "====== Copy assets ======"
|
echo "====== Copy assets ======"
|
||||||
cp -R ./lib/core/assets/* ./lib/dist/core/bundles/assets
|
cp -R ./lib/core/assets/* ./lib/dist/core/bundles/assets
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user